基于 ASM+Maven 插件实现 Java 方法调用链分析
本文介绍如何使用 ASM 字节码解析框架构建 Maven 插件,实现无侵入式的 Java 方法调用链分析。通过解析编译后的 .class 文件,支持包过滤、循环检测及树形可视化输出。适用于老旧代码重构、依赖梳理、死代码识别及故障排查等场景。教程包含环境准备、核心原理、插件开发步骤及测试使用方法。

本文介绍如何使用 ASM 字节码解析框架构建 Maven 插件,实现无侵入式的 Java 方法调用链分析。通过解析编译后的 .class 文件,支持包过滤、循环检测及树形可视化输出。适用于老旧代码重构、依赖梳理、死代码识别及故障排查等场景。教程包含环境准备、核心原理、插件开发步骤及测试使用方法。

本文基于 ASM 字节码解析开发的 Maven 调用链分析插件,实现无侵入式的类方法调用链分析,无需运行业务代码,直接解析编译后的 .class 文件,快速定位指定类的方法调用关系。结合其「包过滤、循环调用检测、树形可视化、字节码级无侵入」等特性,适用于以下场景:
ASM 是轻量级字节码操作框架,核心通过「读取 - 遍历 - 解析」三步识别方法调用:
ClassReader:读取 .class 文件的字节码流;ClassVisitor:遍历类结构(类名、方法、字段);MethodVisitor:遍历方法内的指令,识别 invokevirtual/invokespecial/invokestatic/invokeinterface 等方法调用指令。插件整体执行逻辑:
.class 文件;.class,通过 ASM 解析类内所有方法及方法调用指令;call-chain-analyzer-maven-plugin)org.apache.maven.archetypes:maven-archetype-plugin(Maven 官方提供的插件项目模板)点击右下角的'创建'按钮,Maven 会自动完成插件项目初始化,项目结构如下。
call-chain-analyzer-maven-plugin)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 项目。
创建 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>
创建 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);
}
}
继承 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);
}
}
Mojo 是 Maven 插件的核心执行入口类(Mojo = Maven Old Java Object,等价于插件的'main 类'),所有插件逻辑都围绕这个类展开。
创建 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() 是插件的入口,流程如下:
@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);
}
递归遍历 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());
}
}
通过 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
根据全量 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()))); // 映射值:被调用方列表
}
根据 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));
}
递归打印树形调用链,并通过 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 控制树形缩进(如 ├──/└──),保证输出格式清晰;在插件项目根目录执行:
mvn clean install
在需要解析调用链的业务项目 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>
打开终端执行以下命令:
mvn call-chain-analyzer:analyze -f pom.xml
或在 IDEA 右侧的 Maven 面板「插件」中找到该插件,双击执行。
插件会在控制台输出目标类的方法调用链。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online