precise-testing-ASM
基于 Java ASM 字节码框架的方法调用链分析工具,能够从 .class 文件或 JAR 包中递归构建方法调用链的 DAG 结构。
项目简介
precise-testing-ASM 是一个轻量级的字节码分析工具,专注于方法调用链的追踪与可视化。它能够:
- 从编译后的 .class 文件或 JAR 包中解析字节码
- 递归构建方法调用关系的 DAG(有向无环图)结构
- 支持简单的多态解析、Lambda 表达式追踪、循环调用检测等复杂场景
- 自动过滤 JDK 自带类,只关注业务代码的调用链路
技术栈
- Java
- ASM 依赖
- 构建工具 Maven
快速上手
完全可以参考 src/main/java/Main.java 中的示例代码,快速上手使用
1. 引入依赖
java
import org.example.CallChainAnalyzer;
import org.example.node.DagNode;
import org.example.traverser.DagTraverser;
2. 分析 JAR 包中的方法调用链
java
// 创建分析器,传入 JAR 包路径和解压目录
CallChainAnalyzer analyzer = new CallChainAnalyzer(
"path/to/application.jar",
"extracted/classes"
);
// 分析指定方法的调用链
DagNode root = analyzer.analyze(
"com.example.StudentListServlet", // 类名
"handle", // 方法名
Arrays.asList( // 参数类型列表
"javax.servlet.http.HttpServletRequest",
"javax.servlet.http.HttpServletResponse"
)
);
// 打印调用链树形结构
DagTraverser traverser = new DagTraverser();
traverser.printTree(root);
3. 分析目录中的 .class 文件
java
// 创建分析器,传入包含 .class 文件的目录路径
CallChainAnalyzer analyzer = new CallChainAnalyzer("path/to/classes");
// 分析方法调用链
DagNode root = analyzer.analyze(
"com.example.MyClass",
"myMethod",
Arrays.asList("java.lang.String", "int")
);
// 打印调用链
DagTraverser traverser = new DagTraverser();
traverser.printTree(root);
当然 DagTraverser 中也有其他方法,方便你去遍历方法调用链
4. 输出示例
分析 StudentListServlet#handle 方法后的树形结构:
StudentListServlet#handle(HttpServletRequest, HttpServletResponse)
├── StudentDao#query(String, String, Integer, String, int, int)
│ ├── StudentDao#lambda$query$0(String, Student)
│ │ └── Student#getId()
│ ├── StudentDao#lambda$query$1(String, Student)
│ │ └── Student#getName()
│ └── PageResult#PageResult(List, int, int, long)
├── ApiResponse#success(Object)
│ └── ApiResponse#ApiResponse(int, String, Object)
├── StudentListServlet#writeJson(HttpServletResponse, Object)
│ └── Gson#toJson(Object)
├── ApiResponse#error(int, String)
│ └── ApiResponse#ApiResponse(int, String, Object)
└── StudentListServlet#writeJson(HttpServletResponse, Object)
└── Gson#toJson(Object)
详细使用指南
CallChainAnalyzer(入口类)
CallChainAnalyzer 是整个工具的入口,负责初始化字节码解析环境和执行分析。
构造方式
方式一:从目录加载
java
CallChainAnalyzer analyzer = new CallChainAnalyzer(String bytecodePath);
bytecodePath: 包含 .class 文件的目录路径- 适用于已解压的项目编译输出目录
方式二:从 JAR 包加载
java
CallChainAnalyzer analyzer = new CallChainAnalyzer(String jarPath, String extractDir);
jarPath: JAR 包文件路径extractDir: 解压目标目录,工具会自动将 JAR 内容解压到此目录- 适用于分析打包后的应用程序
初始化方法
java
analyzer.init();
- 扫描指定路径下的所有 .class 文件
- 构建类的继承关系图谱
- 注意:首次调用
analyze()时会自动调用init(),也可手动提前调用
分析方法
java
DagNode root = analyzer.analyze(String className, String methodName, List<String> paramTypes);
className: 完整类名(如com.example.MyClass)methodName: 方法名paramTypes: 参数类型列表,使用完整类名(如java.lang.String)- 返回值:调用链的根节点
DagNode
DagNode(调用链节点)
每个 DagNode 代表调用链中的一个方法节点,包含以下信息:
| 字段 | 类型 | 说明 |
|---|---|---|
declClass |
String | 声明类,多态场景下变量的声明类型 |
implClass |
String | 实现类,运行时实际执行的类型 |
funcName |
String | 方法名 |
funcParams |
List<String> | 参数类型列表 |
funcReturnType |
String | 返回类型 |
isloop |
boolean | 是否为循环调用节点 |
children |
List<DagNode> | 子节点列表,当前方法调用的其他方法 |
parents |
List<DagNode> | 父节点列表,调用当前方法的节点 |
DagTraverser(遍历器)
DagTraverser 提供了多种遍历和查询调用链的方式。
打印树形结构
java
DagTraverser traverser = new DagTraverser();
traverser.printTree(root);
从根节点向下遍历,以树形格式打印整个调用链。
查找根节点
java
Map<DagNode, List<DagNode>> roots = traverser.findRoots(nodeList);
从任意节点列表向上追溯,找到每个节点的根节点。
先序遍历
java
List<DagNode> nodes = traverser.preorderTraversal(root);
按先序遍历收集所有节点,顺序为:根节点 -> 左子树 -> 右子树。
后序遍历
java
List<DagNode> nodes = traverser.postorderTraversal(root);
按后序遍历收集所有节点,顺序为:左子树 -> 右子树 -> 根节点。
MethodSignature(方法签名)
用于唯一标识一个方法的签名对象,包含类名、方法名和参数类型列表。
项目结构
src/main/java/org/example/
├── CallChainAnalyzer.java # 入口类,提供分析器的构造和初始化
├── Main.java # Demo 示例,展示工具的基本用法
├── builder/
│ └── CallChainBuilder.java # 调用链构建器,核心递归逻辑实现
├── graph/
│ └── InheritanceGraph.java # 继承关系图谱,支持多态解析
├── model/
│ └── MethodSignature.java # 方法签名模型,唯一标识方法
├── node/
│ └── DagNode.java # DAG 节点,表示调用链中的方法节点
├── parser/
│ └── BytecodeParser.java # 字节码解析器,读取 .class 文件
├── traverser/
│ └── DagTraverser.java # DAG 遍历器,提供多种遍历方式
└── util/
└── TypeUtils.java # 类型描述符工具类,处理字节码类型转换
支持的复杂场景
本工具能够处理以下字节码分析中的复杂场景:
1. 多态调用
接口或父类引用指向子类实例时,工具能够推断实际的实现类。
java
InterfaceA a = new ImplB();
a.doSomething(); // 工具会解析出实际调用的是 ImplB#doSomething
2. Lambda 表达式
Java 8+ 的 Lambda 表达式编译为 invokedynamic 指令,工具会解析 LambdaMetafactory 获取实际的实现方法。
java
list.stream()
.filter(item -> item.isValid()) // 解析为 lambda$filter$0 方法
.collect(Collectors.toList());
3. 循环调用检测
自动检测方法间的递归或相互调用,标记循环节点,避免无限递归分析。
A -> B -> A (循环)
工具会在第二个 A 节点标记 isloop=true,停止继续递归
4. 节点共享
同一方法被多处调用时,复用同一个 DagNode 实例,构建 DAG 而非树结构,节省内存并保持调用关系的准确性。
5. 构造方法调用
正确解析 <init> 特殊方法,追踪对象创建过程中的构造函数调用链。
6. 静态代码块过滤
自动过滤 <clinit> 静态初始化块,避免追踪类加载时的初始化逻辑,只关注业务方法调用。
7. JDK 类过滤
自动跳过 java.、javax.、jdk. 等 JDK 自带类的调用,只分析业务代码的调用链路,减少噪音。
8. 泛型擦除处理
字节码中泛型信息已被擦除,工具能够正确处理不含泛型的类型描述符,确保类型匹配的准确性。
运行 Demo
项目自带一个示例 JAR 包,位于 src/main/resources/test/java-web-demo-1.0-SNAPSHOT.jar。
运行 Main.java 即可看到完整的分析效果:
bash
mvn compile exec:java -Dexec.mainClass="org.example.Main"
或直接在 IDE 中运行 Main 类。
总结
precise-testing-ASM 提供了一种从字节码层面分析方法调用链的方案,能够处理多态、Lambda、循环调用等复杂场景。它适用于:
- 理解复杂项目的代码调用关系
- 代码重构前的影响范围分析
- 测试覆盖率分析时确定需要覆盖的调用路径
- 技术文档生成时的方法依赖梳理
工具设计简洁,API 易用,只需几行代码即可完成调用链的分析和可视化。