基于 ASM+Maven 插件实现 Java 方法调用链分析
本文基于 ASM 字节码解析开发的 Maven 调用链分析插件,实现无侵入式的类方法调用链分析,无需运行业务代码,直接解析编译后的 .class 文件,快速定位指定类的方法调用关系。结合其「包过滤、循环调用检测、树形可视化、字节码级无侵入」等特性,适用于以下场景:
- 老旧代码重构:解析调用链、标记循环调用,明确修改边界
- 大型项目依赖梳理:梳理模块间方法依赖,定位关键调用节点
- 死代码识别:检测无调用的方法,辅助代码瘦身
- 故障排查:快速追溯异常方法的上下游调用链
- 接口变更评估:识别方法的调用方,评估变更影响范围
- 代码合规审计:检查核心类是否调用违规方法 / 第三方包
- 性能瓶颈定位:识别调用层级过深(如超过 5 层)、循环调用等性能问题
一、前置准备
1.1 开发环境
- JDK 21+(ASM 对 JDK 8 兼容性最佳,也是企业级项目主流版本)
- Maven 3.8+(插件开发基础依赖)
- IDE(IntelliJ IDEA/Eclipse,推荐 IDEA 自带 Maven 插件调试功能)
1.2 ASM 核心认知
ASM 是轻量级字节码操作框架,核心通过「读取 - 遍历 - 解析」三步识别方法调用:
ClassReader:读取.class文件的字节码流;ClassVisitor:遍历类结构(类名、方法、字段);MethodVisitor:遍历方法内的指令,识别invokevirtual/invokespecial/invokestatic/invokeinterface等方法调用指令。
二、插件核心原理
插件整体执行逻辑:
- 遍历指定目录下的所有
.class文件; - 对每个
.class,通过 ASM 解析类内所有方法及方法调用指令; - 构建「调用方 → 被调用方」的映射关系,存储调用链;
- 对调用链做过滤、循环检测、可视化输出。
三、Maven 插件开发
3.1 初始化 Maven 插件项目
方式一:用 Archetype 创建插件项目
- 名称:填写项目名(如示例中的
call-chain-analyzer-maven-plugin) - JDK:选择项目使用的 JDK 版本(如示例中的 Oracle OpenJDK 21)
- Archetype:选择
org.apache.maven.archetypes:maven-archetype-plugin(Maven 官方提供的插件项目模板)
点击右下角的'创建'按钮,Maven 会自动完成插件项目初始化,项目结构如下。
方式二:创建普通 Java 项目
- 名称:填写项目名(如示例中的
call-chain-analyzer-maven-plugin) - JDK:选择项目使用的 JDK 版本(如示例中的 Oracle OpenJDK 21)
- 构建系统:勾选'Maven'
- 执行
mvn clean compile验证依赖是否正常(无报错即成功)。
在 pom.xml 中添加 Maven 插件的核心配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 1. 项目基本信息 -->
<groupId>com.example</groupId>
<artifactId>call-chain-analyzer-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<!-- 关键:打包类型为 maven-plugin -->
<!-- 2. 依赖:Maven 插件核心 API -->
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.9.11</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.9.11</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.15.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- 3. 插件配置:编译注解 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.15.1</version>
<executions>
<execution>
<id>default-descriptor</id>
<phase>process-classes</phase>
</execution>
</executions>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
<packaging>maven-plugin</packaging>用于声明项目为 Maven 插件,是插件项目的核心标识,缺失则无法被 Maven 识别
打开 IDEA → 选择'新建项目',在左侧菜单中点击'Java',填写项目基本信息,生成一个空的 Java 项目。
3.2 定义数据模型(MethodCallInfo)
创建 pojo/MethodCallInfo.java,用于存储单次调用'调用方'和'被调用方'的信息,是整个插件的数据载体:
/**
* 方法调用信息模型:存储类方法单次调用的「调用方 - 被调用方」关系
*/
@Data
@AllArgsConstructor
public class MethodCallInfo {
// 调用方类全限定名(如 com.example.demo.UserService)
private String callerClass;
// 调用方方法名(如 getUser)
private String callerMethod;
// 调用方方法描述符
private String callerMethodDesc;
// 被调用方类全限定名
private String calleeClass;
// 被调用方方法名
private String calleeMethod;
// 被调用方方法描述符
private String calleeMethodDesc;
// 格式化输出调用方方法
public String getCallerMethodFull() {
return callerClass + "#" + callerMethod + callerMethodDesc;
}
// 格式化输出被调用方方法
public String getCalleeMethodFull() {
return calleeClass + "#" + calleeMethod + calleeMethodDesc;
}
}
添加构建配置,显式声明 Lombok 为注解处理器:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>21</source>
<target>21</target>
<!-- 显式声明 Lombok 为注解处理器 -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
在 pom.xml 中添加 Lombok 依赖:
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
3.3 实现 ASM 解析逻辑
类级别访问器(ClassCallVisitor)
创建 ClassCallVisitor 类,继承 ASM 的 ClassVisitor,负责类层面的遍历与过滤,为符合条件的方法创建方法级访问器:
/**
* 类级别的 ASM 解析器:遍历类结构,处理每个方法
*/
public class ClassCallVisitor extends ClassVisitor {
private final Set<MethodCallInfo> methodCallSet;
private String currentClass;
private final List<String> scanPackages;
private final List<String> excludeScanPackages;
public ClassCallVisitor(int api, Set<MethodCallInfo> methodCallSet, List<String> scanPackages, List<String> excludeScanPackages) {
super(api);
this.methodCallSet = methodCallSet;
this.scanPackages = scanPackages;
this.excludeScanPackages = excludeScanPackages;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// 将 ASM 格式类名(com/example/OrderService)转为全限定名(com.example.OrderService)
this.currentClass = name.replace("/", ".");
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 跳过排除的包
if (excludeScanPackages.stream().anyMatch(ignorePkg -> currentClass.startsWith(ignorePkg.trim() + "."))) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
// 跳过抽象方法、native 方法(无字节码,无法解析调用)
if ((access & Opcodes.ACC_ABSTRACT) != 0 || (access & Opcodes.ACC_NATIVE) != 0) {
return methodVisitor;
}
// 解析方法内的调用指令
return new MethodCallVisitor(Opcodes.ASM9, methodVisitor, currentClass, name, context);
}
}
方法级别访问器(MethodCallVisitor)
继承 ASM 的 MethodVisitor,负责方法内调用指令的解析与过滤,最终仅收集满足以下所有条件的方法调用关系:
- 调用方 / 被调用方均不属于'忽略包';
- 调用方不是构造方法 / 静态初始化方法;
- 被调用方不是构造方法 / 静态初始化方法;
- 若配置了'扫描包',被调用方必须属于'扫描包'范围(未配置则无此限制)。
/**
* 方法级别的 ASM 解析器:识别方法内的调用指令,构建调用链
*/
public class MethodCallVisitor extends MethodVisitor {
// 调用方类名(全限定名)
private final String callerClass;
// 调用方方法名
private final String callerMethod;
// 全局上下文
private final CallChainContext context;
public MethodCallVisitor(int api, MethodVisitor methodVisitor, String callerClass, String callerMethod, CallChainContext context) {
super(api, methodVisitor);
this.callerClass = callerClass;
this.callerMethod = callerMethod;
this.context = context;
}
/**
* 核心方法:解析方法调用指令
* @param opcode 调用指令类型(invokevirtual/invokestatic 等)
* @param owner 被调用类的 ASM 格式名称(如 com/example/OrderDao)
* @param name 被调用方法名
* @param descriptor 方法描述符(如 (Ljava/lang/String;)V)
* @param isInterface 是否是接口方法
*/
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
// 1. 转换被调用类名为全限定名
String calleeClass = owner.replace("/", ".");
// 2. 包过滤:只处理指定包下的被调用类
Set<String> includePackages = context.getIncludePackages();
if (includePackages != null && !includePackages.isEmpty()) {
boolean match = includePackages.stream().anyMatch(pkg -> calleeClass.startsWith(pkg));
if (!match) {
return;
}
}
// 3. 跳过构造方法/静态初始化方法(可选,根据业务需求调整)
if ("<init>".equals(name) || "<clinit>".equals(name)) {
return;
}
// 4. 存储调用关系
MethodCall methodCall = new MethodCall(callerClass, callerMethod, calleeClass, name);
context.addMethodCall(methodCall);
}
}
3.4 开发插件入口 Mojo 类
Mojo 是 Maven 插件的核心执行入口类(Mojo = Maven Old Java Object,等价于插件的'main 类'),所有插件逻辑都围绕这个类展开。
创建 AnalyzeCallChainMojo 类
创建 AnalyzeCallChainMojo 类整合「遍历.class 文件 → 触发 ASM 解析 → 输出结果」全流程,代码如下:
/**
* Maven 插件核心 Mojo:方法调用链分析
* Goal: analyze(调用命令:mvn call-chain:analyze)
*/
@Mojo(name = "analyze", threadSafe = true, defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.COMPILE)
@Execute(phase = LifecyclePhase.COMPILE)
public class AnalyzeCallChainMojo extends AbstractMojo {
// ========== Maven 注入参数 ==========
/** Maven 项目对象(自动注入) */
@Parameter(defaultValue = "${project}", readonly = true)
protected MavenProject project;
/** 扫描的.class 文件根目录(默认:target/classes) */
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
private String classRootDir;
/** 需扫描的包名列表 */
@Parameter(name = "scanPackages", property = "scanPackages")
private List<String> scanPackages = new ArrayList<>();
/** 排除扫描的包名列表 */
@Parameter(name = "excludeScanPackages", property = "excludeScanPackages")
private List<String> excludeScanPackages = new ArrayList<>();
/** 目标分析类全限定名(必填) */
@Parameter(name = "targetClass", property = "targetClass", required = true)
private String targetClass;
// 核心执行方法(后续实现)
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
// ...
}
}
该类继承 AbstractMojo(Maven 插件基类),@Mojo 是 Maven 插件开发的核心注解,每个参数都对应插件的核心行为配置:
name = "analyze":插件的 Goal 名称,是调用插件的核心标识,调用插件时执行命令mvn call-chain:analyze。threadSafe = true:标记插件是否线程安全,设置为true时,避免 Maven 在并行构建(如mvn -T 4 clean compile)时报错。defaultPhase = LifecyclePhase.COMPILE:插件默认绑定的 Maven 生命周期阶段,因为插件需要解析编译后的.class 文件,所以必须在编译完成后执行。requiresDependencyResolution = ResolutionScope.COMPILE:指定插件执行前需要解析的依赖范围,Maven 会先解析项目compile范围的依赖(如 ASM、Maven Plugin API),再执行插件;确保插件运行时依赖已就绪,避免ClassNotFound异常。
@Execute 注解是对 @Mojo 的补充,用于强制 Maven 执行插件之前,会先执行 COMPILE 阶段(编译项目生成.class 文件),避免因跳过编译(如 mvn call-chain:analyze -Dmaven.compile.skip=true)导致插件找不到.class 文件的问题。
@Parameter 是 Maven Plugin API 提供的核心注解,Maven 框架会自动将 pom.xml 中配置的插件参数注入到 Mojo 类的成员变量中。
实现核心执行流程(execute)
execute() 是插件的入口,流程如下:
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
// 1. 初始化参数,避免空集合 NPE
scanPackages = Optional.ofNullable(scanPackages).orElseGet(ArrayList::new);
excludeScanPackages = Optional.ofNullable(excludeScanPackages).orElseGet(ArrayList::new);
// 2. 校验扫描目录合法性
validateClassRootDir();
// 3. 打印扫描配置(便于调试)
logScanConfig();
// 4. 解析所有.class 文件,提取调用关系
Set<MethodCallInfo> allMethodCallInfos = parseAllClassFiles();
// 5. 构建调用关系映射(调用方→被调用方列表)
Map<String, List<String>> methodCallRelationMap = buildMethodCallRelationMap(allMethodCallInfos);
// 6. 筛选目标类的调用关系
Map<String, List<String>> targetClassCallRelationMap = filterTargetClassCallRelations(methodCallRelationMap);
// 7. 输出树形调用链结果
printCallChainResult(targetClass, targetClassCallRelationMap);
}
扫描 class 文件(traverseClassFiles + scanClassFilesRecursive)
递归遍历 classRootDir 目录下所有 .class 文件,收集文件路径:
/**
* 递归遍历.class 文件,返回所有 class 文件路径
*/
private List<String> traverseClassFiles(String dirPath) throws MojoExecutionException {
File scanDir = new File(dirPath);
if (!scanDir.exists() || !scanDir.isDirectory()) {
throw new MojoExecutionException(String.format("扫描路径非法:%s", dirPath));
}
List<String> classFilePaths = new ArrayList<>();
scanClassFilesRecursive(scanDir, classFilePaths);
return Collections.unmodifiableList(classFilePaths);
}
/**
* 递归扫描.class 文件
*/
private void scanClassFilesRecursive(File file, List<String> result) {
if (file.isDirectory()) {
File[] childFiles = file.listFiles();
if (childFiles == null) {
getLog().warn(String.format("无法访问目录:%s", file.getAbsolutePath()));
return;
}
for (File child : childFiles) {
scanClassFilesRecursive(child, result);
}
} else if (file.getName().endsWith(CLASS_FILE_SUFFIX)) {
result.add(file.getAbsolutePath());
}
}
解析 class 文件(parseClassFile)
通过 ASM 的 ClassReader 和自定义 ClassCallVisitor 解析 .class 文件,提取方法调用关系:
/**
* 解析.class 文件,提取方法调用关系
*/
private Set<MethodCallInfo> parseClassFile(String classFilePath, List<String> scanPackages, List<String> excludeScanPackages) throws MojoExecutionException {
try (FileInputStream fis = new FileInputStream(classFilePath)) {
ClassReader classReader = new ClassReader(fis);
Set<MethodCallInfo> methodCallSet = new HashSet<>();
// 自定义 ClassVisitor,解析类结构并捕获调用关系
ClassCallVisitor classVisitor = new ClassCallVisitor(ASM_API_VERSION, methodCallSet, scanPackages, excludeScanPackages);
// 执行解析:跳过调试信息和栈帧,提升效率
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return methodCallSet;
} catch (IOException e) {
String errorMsg = String.format("解析类文件失败:%s", classFilePath);
throw new MojoExecutionException(errorMsg, e);
}
}
ClassCallVisitor是自定义的 ASM Visitor,核心逻辑是遍历方法指令,捕获INVOKEVIRTUAL/INVOKESTATIC等调用指令,封装为MethodCallInfo
构建调用关系(buildMethodCallRelationMap)
根据全量 MethodCallInfo 构建「调用方方法全限定名→被调用方列表」的映射:
/**
* 构建调用关系映射(调用方→被调用方列表)
*/
public static Map<String, List<String>> buildMethodCallRelationMap(Collection<MethodCallInfo> methodCallInfos) {
if (Objects.isNull(methodCallInfos) || methodCallInfos.isEmpty()) {
return Collections.emptyMap();
}
return methodCallInfos.stream().distinct()
// 去重重复的调用关系
.collect(Collectors.groupingBy(MethodCallInfo::getCallerMethodFull,
Collectors.mapping(MethodCallInfo::getCalleeMethodFull, Collectors.toList()))); // 映射值:被调用方列表
}
筛选目标类的调用关系(filterTargetClassCallRelations)
根据 buildMethodCallRelationMap() 构建的调用关系筛查出目标类中所有方法的调用关系:
/**
* 筛选目标类的调用关系(按类名前缀过滤)
*/
private Map<String, List<String>> filterTargetClassCallRelations(Map<String, List<String>> methodCallRelationMap) {
if (Objects.isNull(methodCallRelationMap) || methodCallRelationMap.isEmpty()) {
return Collections.emptyMap();
}
String targetClassPrefix = targetClass + METHOD_FULL_NAME_SEPARATOR;
// 筛选 + 排序
return methodCallRelationMap.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(targetClassPrefix))
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream().sorted().toList(), (v1, v2) -> v1, TreeMap::new));
}
树形输出与循环检测(printMethodCallChain)
递归打印树形调用链,并通过 visitedMethods 集合检测循环调用:
/**
* 递归打印方法调用链
*/
private void printMethodCallChain(String currentMethod, List<String> calleeList, String prefix, boolean isLastNode, Map<String, List<String>> relationMap, Set<String> visitedMethods) {
// 空值保护(避免 NPE)
if (Objects.isNull(currentMethod) || currentMethod.trim().isEmpty()) {
return;
}
// ========== 循环调用检测(传递引用 + 回溯) ==========
if (visitedMethods.contains(currentMethod)) {
String symbol = isLastNode ? TREE_LAST_NODE_SYMBOL : TREE_NODE_SYMBOL;
getLog().info(String.format("%s%s%s%s", prefix, symbol, currentMethod, " 「循环调用」"));
return;
}
// 标记当前方法为已访问
visitedMethods.add(currentMethod);
// ========== 打印当前节点 ==========
String nodeSymbol = isLastNode ? TREE_LAST_NODE_SYMBOL : TREE_NODE_SYMBOL;
getLog().info(String.format("%s%s%s", prefix, nodeSymbol, currentMethod));
// ========== 处理子节点 ==========
List<String> parsedCalleeList = Optional.ofNullable(calleeList).map(ArrayList::new)
// 避免修改原列表
.orElse(new ArrayList<>()).stream()
.filter(Objects::nonNull)
// 过滤 null 的被调用方
.sorted(Comparator.naturalOrder())
.toList();
if (parsedCalleeList.isEmpty()) {
// 回溯:移除当前方法,避免影响兄弟节点检测
visitedMethods.remove(currentMethod);
return;
}
// 构建子节点前缀
String childPrefix = prefix + (isLastNode ? TREE_LAST_CHILD_PREFIX : TREE_CHILD_PREFIX);
// 遍历子节点递归打印
for (int i = 0; i < parsedCalleeList.size(); i++) {
String calleeMethod = parsedCalleeList.get(i);
boolean isLastChild = (i == parsedCalleeList.size() - 1);
List<String> nextCalleeList = Optional.ofNullable(relationMap.get(calleeMethod)).orElse(Collections.emptyList());
// 传递同一个 visitedMethods 引用(核心:实现深层循环检测)
printMethodCallChain(calleeMethod, nextCalleeList, childPrefix, isLastChild, relationMap, visitedMethods);
}
// ========== 回溯:移除当前方法,完成循环检测闭环 ==========
visitedMethods.remove(currentMethod);
}
核心逻辑:
- 通过
visitedMethods集合记录已访问的方法,检测到循环调用时标记并终止递归; - 递归遍历子节点时,通过
prefix控制树形缩进(如├──/└──),保证输出格式清晰; - 回溯移除当前方法,确保兄弟节点的循环检测不受影响。
四、插件测试与使用
4.1 安装插件到本地仓库
在插件项目根目录执行:
mvn clean install
4.2 在业务项目中引入插件
在需要解析调用链的业务项目 pom.xml 中添加插件配置:
<build>
<plugins>
<plugin>
<groupId>com.shijie.plugin</groupId>
<artifactId>call-chain-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<!-- 必填:目标分析类全限定名 -->
<targetClass>com.shijie.service.UserService</targetClass>
<!-- 可选:扫描的包名列表 -->
<scanPackages>
<scanPackage>com.shijie.service</scanPackage>
<scanPackage>com.shijie.dao</scanPackage>
</scanPackages>
<!-- 可选:排除的包名列表 -->
<excludeScanPackages>
<excludeScanPackage>com.shijie.test</excludeScanPackage>
</excludeScanPackages>
</configuration>
</plugin>
</plugins>
</build>
4.3 执行插件
打开终端执行以下命令:
mvn call-chain-analyzer:analyze -f pom.xml
或在 IDEA 右侧的 Maven 面板「插件」中找到该插件,双击执行。
插件会在控制台输出目标类的方法调用链。


