背景
大家都知道Spring容器启动时,主要通过调用org.springframework.context.support.AbstractApplicationContext#refresh
方法,但是在调用过程中,有很多的调用链,分析起来很麻烦,出于这个目的,我打算写个小插件,Spring容器初始化的调用堆栈。
思路
有了这个背景后,就要思考怎么能打印出来调用的堆栈信息,接触的有两种方式
- 通过
java.lang.Thread#getAllStackTraces
方法获取调用堆栈 - 改写字节码
首页看第一种,第一种方式,可以在当前的位置,打印出到达这个方法的堆栈信息,貌似可以满足我们的需求。但是这个只能打印我需要知道的地方的调用堆栈,也就是说,我要前提很明确会调用到哪里,这样显然不能满足我们的需求。
再看第二种方式,通过改写字节码的方式,通过改写字节码,可以在方法进入时,打印方法名称,这样可以知道调用了哪些方法,这样可以满足我们的需求。目前改写字节码的方式主要通过javaagent或asm的方式,我们尝试通过javaagent的方式来完成我们的需求。
具体方法
确定了思路后,开始写一个javaagent,关于javaagent的具体介绍不是本文重点,不做介绍了,可以参考相关文章
主要有3个文件,JavaAgent.java、DefineTransformer.java、PrintMethod.java,具体代码如下:
// JavaAgent.java
public class JavaAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new DefineTransformer(), true);
}
}
// DefineTransformer.java
public class DefineTransformer implements ClassFileTransformer {
final static String enterPattern = "com.ethan.javaagent.PrintMethod.enterMethod(\"%s\");";
final static String leavePattern = "com.ethan.javaagent.PrintMethod.leaveMethod(\"%s\");";
// 被处理的方法列表
final static List<String> wroteMethod = new ArrayList<>();
static List<String> includePackages;
DefineTransformer(String[] args) {
includePackages = Arrays.asList(args);
System.out.println(includePackages);
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
// 此处可以调整,此处意思是修改org.springframework包下的字节码,不修改org.springframework.util包下的字节码
if (className.startsWith("org.springframework") && !className.startsWith("org.springframework.util")) {
CtClass ctclass = null;
try {
ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
for (CtMethod ctMethod : ctclass.getMethods()) {
if (Modifier.isAbstract(ctMethod.getModifiers())) {
continue;
}
if (Modifier.isNative(ctMethod.getModifiers())) {
continue;
}
if (!ctMethod.getLongName().startsWith(className)) {
continue;
}
if (wroteMethod.contains(ctMethod.getLongName())) {
continue;
}
wroteMethod.add(ctMethod.getLongName());
ctMethod.insertBefore(String.format(enterPattern, ctMethod.getLongName()));
ctMethod.insertAfter(String.format(leavePattern, ctMethod.getLongName()), true);
}
return ctclass.toBytecode();
} catch (Throwable e) {
System.out.println("e.getMessage()" +e.getMessage());
e.printStackTrace();
}
}
return classfileBuffer;
}
}
//PrintMethod.java
public class PrintMethod {
private static volatile int stack = 0;
public static void enterMethod(String methodName) {
for (int i = 0; i < stack; i++) {
System.out.print(" ");
}
System.out.println(methodName);
stack ++;
}
public static void leaveMethod(String methodName) {
stack --;
}
}
以上三个文件是javaagent文件,算是完成了javaagent的代码部分,下面打包部分,也在resources下添加META-INF/MANIFEST.MF文件,文件内容如下:
Manifest-Version: 1.0
Premain-Class: com.ethan.javaagent.JavaAgent
Agent-Class: com.ethan.javaagent.JavaAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
打包agent.jar
详细代码参见:github
验证
编写一个简单的main方法,验证结果:
public class ClassPathBootstrap {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:classpath-user.xml");
}
}
使用IDEA运行时,在VM Options中,输入-javaagent:agent.jar
, 其中agent.jar
写上述打包的绝对路径
部分输出结果如下
详细代码参见:github