代码重构怕踩坑?基于 ASM+Maven 插件实现分析 Java 方法调用链(附完整代码)
本文基于 ASM 字节码解析开发的 Maven 调用链分析插件,实现无侵入式的类方法调用链分析,无需运行业务代码,直接解析编译后的 .class 文件,快速定位指定类的方法调用关系,结合其「包过滤、循环调用检测、树形可视化、字节码级无侵入」等特性,适用于以下场景:
- 老旧代码重构:解析调用链、标记循环调用,明确修改边界
- 大型项目依赖梳理:梳理模块间方法依赖,定位关键调用节点
- 死代码识别:检测无调用的方法,辅助代码瘦身
- 故障排查:快速追溯异常方法的上下游调用链
- 接口变更评估:识别方法的调用方,评估变更影响范围
- 代码合规审计:检查核心类是否调用违规方法 / 第三方包
- 性能瓶颈定位:识别调用层级过深(如超过 5 层)、循环调用等性能问题
该教程全程实操导向,所有代码均可直接复制运行,每个步骤都附带详细的字节码解析逻辑和插件开发细节说明。哪怕你是 Maven 插件开发新手,也能跟着步骤快速落地这款工具,让代码重构的依赖梳理环节效率翻倍、少踩坑!
项目源码地址:call-chain-analyzer-maven-plugin(可直接克隆源码运行、调试或二次开发)
一、前置准备
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 解析类内所有方法及方法调用指令; - 构建「调用方 → 被调用方」的映射关系,存储调用链;
- 对调用链做过滤、循环检测、可视化输出。
扫描编译目录下的 .class 文件
ASM 解析 .class 内所有方法及方法调用指令
构建 调用方-被调用方 的映射关系
调用链筛选目标类的调用关系
可视化输出
三、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 会自动完成插件项目初始化,项目结构如下:
在左侧菜单中点击 Maven Archetype,填写项目基本信息:

打开 IDEA(或其他 IDE),点击 “新建项目”:
方式二:创建普通 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"?><projectxmlns="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 识别点击右下角的 “创建” 按钮,生成一个空的 Java 项目。
在左侧菜单中点击 “Java”,填写项目基本信息:
打开 IDEA → 选择 “新建项目”:
3.2 定义数据模型(MethodCallInfo)
创建 pojo/MethodCallInfo.java,用于存储单次调用 “调用方” 和 “被调用方” 的信息,是整个插件的数据载体:
/** * 方法调用信息模型:存储类方法单次调用的「调用方-被调用方」关系 */@Data@AllArgsConstructorpublicclassMethodCallInfo{// 调用方类全限定名(如 com.example.demo.UserService)privateString callerClass;// 调用方方法名(如 getUser)privateString callerMethod;// 调用方方法描述符privateString callerMethodDesc;// 被调用方类全限定名privateString calleeClass;// 被调用方方法名privateString calleeMethod;// 被调用方方法描述符privateString calleeMethodDesc;// 格式化输出调用方方法publicStringgetCallerMethodFull(){return callerClass +"#"+ callerMethod + callerMethodDesc;}// 格式化输出被调用方方法publicStringgetCalleeMethodFull(){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 解析逻辑
类级别访问器(ClassCallChainVisitor)
创建 ClassCallChainVisitor 类,继承 ASM 的ClassVisitor,负责类层面的遍历与过滤,为符合条件的方法创建方法级访问器:
/** * 类级别的 ASM 解析器:遍历类结构,处理每个方法 */publicclassClassCallVisitorextendsClassVisitor{privatefinalSet<MethodCallInfo> methodCallSet;privateString currentClass;privatefinalList<String> scanPackages;privatefinalList<String> excludeScanPackages;publicClassCallVisitor(int api,Set<MethodCallInfo> methodCallSet,List<String> scanPackages,List<String> excludeScanPackages){super(api);this.methodCallSet = methodCallSet;this.scanPackages = scanPackages;this.excludeScanPackages = excludeScanPackages;}publicClassCallChainVisitor(ClassVisitor classVisitor,CallChainContext context){super(Opcodes.ASM9, classVisitor);this.context = context;}@Overridepublicvoidvisit(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);}@OverridepublicMethodVisitorvisitMethod(int access,String name,String descriptor,String signature,String[] exceptions){// 跳过排除的包if(excludeScanPackages.stream().anyMatch(ignorePkg ->this.currentClass.startsWith(ignorePkg.trim()+"."))){returnsuper.visitMethod(access, name, desc, 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;}// 解析方法内的调用指令returnnewMethodCallVisitor(Opcodes.ASM9, methodVisitor, currentClass, name, context);}}方法级别访问器(MethodCallVisitor)
继承 ASM 的 MethodVisitor,负责方法内调用指令的解析与过滤,最终仅收集满足以下所有条件的方法调用关系:
- 调用方 / 被调用方均不属于 “忽略包”;
- 调用方不是构造方法 / 静态初始化方法;
- 被调用方不是构造方法 / 静态初始化方法;
- 若配置了 “扫描包”,被调用方必须属于 “扫描包” 范围(未配置则无此限制)。
/** * 方法级别的 ASM 解析器:识别方法内的调用指令,构建调用链 */publicclassMethodCallVisitorextendsMethodVisitor{// 调用方类名(全限定名)privatefinalString callerClass;// 调用方方法名privatefinalString callerMethod;// 全局上下文privatefinalCallChainContext context;publicMethodCallVisitor(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 是否是接口方法 */@OverridepublicvoidvisitMethodInsn(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 =newMethodCall(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)publicclassAnalyzeCallChainMojoextendsAbstractMojo{// ========== Maven注入参数 ==========/** Maven项目对象(自动注入) */@Parameter(defaultValue ="${project}", readonly =true)protectedMavenProject project;/** 扫描的.class文件根目录(默认:target/classes) */@Parameter(defaultValue ="${project.build.outputDirectory}", required =true)privateString classRootDir;/** 需扫描的包名列表 */@Parameter(name ="scanPackages", property ="scanPackages")privateList<String> scanPackages =newArrayList<>();/** 排除扫描的包名列表 */@Parameter(name ="excludeScanPackages", property ="excludeScanPackages")privateList<String> excludeScanPackages =newArrayList<>();/** 目标分析类全限定名(必填) */@Parameter(name ="targetClass", property ="targetClass", required =true)privateString targetClass;// 核心执行方法(后续实现)@Overridepublicvoidexecute()throwsMojoExecutionException,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 文件的问题;
@Execute(phase = LifecyclePhase.COMPILE) @Parameter 是 Maven Plugin API 提供的核心注解,Maven 框架会自动 将 pom.xml 中中配置的插件参数 注入到 Mojo 类的成员变量中。
实现核心执行流程(execute)
execute() 是插件的入口,流程如下:
@Overridepublicvoidexecute()throwsMojoExecutionException,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文件路径 */privateList<String>traverseClassFiles(String dirPath)throwsMojoExecutionException{File scanDir =newFile(dirPath);if(!scanDir.exists()||!scanDir.isDirectory()){thrownewMojoExecutionException(String.format("扫描路径非法: %s", dirPath));}List<String> classFilePaths =newArrayList<>();scanClassFilesRecursive(scanDir, classFilePaths);returnCollections.unmodifiableList(classFilePaths);}/** * 递归扫描.class文件 */privatevoidscanClassFilesRecursive(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);}}elseif(file.getName().endsWith(CLASS_FILE_SUFFIX)){ result.add(file.getAbsolutePath());}}解析 class 文件(parseClassFile)
通过 ASM 的 ClassReader 和自定义 ClassCallVisitor 解析 .class 文件,提取方法调用关系:
/** * 解析.class文件,提取方法调用关系 */privateSet<MethodCallInfo>parseClassFile(String classFilePath,List<String> scanPackages,List<String> excludeScanPackages)throwsMojoExecutionException{try(FileInputStream fis =newFileInputStream(classFilePath)){ClassReader classReader =newClassReader(fis);Set<MethodCallInfo> methodCallSet =newHashSet<>();// 自定义ClassVisitor,解析类结构并捕获调用关系ClassCallVisitor classVisitor =newClassCallVisitor(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);thrownewMojoExecutionException(errorMsg, e);}}🐖ClassCallVisitor是自定义的 ASM Visitor,核心逻辑是遍历方法指令,捕获INVOKEVIRTUAL/INVOKESTATIC等调用指令,封装为MethodCallInfo
构建调用关系(buildMethodCallRelationMap)
根据全量 MethodCallInfo 构建「调用方方法全限定名→被调用方列表」的映射:
/** * 构建调用关系映射(调用方→被调用方列表) */publicstaticMap<String,List<String>>buildMethodCallRelationMap(Collection<MethodCallInfo> methodCallInfos){if(Objects.isNull(methodCallInfos)|| methodCallInfos.isEmpty()){returnCollections.emptyMap();}return methodCallInfos.stream().distinct()// 去重重复的调用关系.collect(Collectors.groupingBy(MethodCallInfo::getCallerMethodFull,// 分组键:调用方方法全限定名Collectors.mapping(MethodCallInfo::getCalleeMethodFull,Collectors.toList())// 映射值:被调用方列表));}筛选目标类的调用关系(filterTargetClassCallRelations)
根据 buildMethodCallRelationMap() 构建的调用关系筛查出目标类中所有方法的调用关系:
/** * 筛选目标类的调用关系(按类名前缀过滤) */privateMap<String,List<String>>filterTargetClassCallRelations(Map<String,List<String>> methodCallRelationMap){if(Objects.isNull(methodCallRelationMap)|| methodCallRelationMap.isEmpty()){returnCollections.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集合检测循环调用:
/** * 递归打印方法调用链 */privatevoidprintMethodCallChain(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(newArrayList<>()).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 install4.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 面板「插件」中找到该插件,双击执行:
插件会在控制台输出目标类的方法调用链,示例如下: