概述
本文介绍javaagent规范和通过asm修改字节码。并实现一个简单的统计方法执行时间的agent。
javaagent规范
java 提供了操作运行时字节码的机制,见包java.lang.instrument
。可以用java开发一个jar包,以agent方式部署。比如jar包myAgent.jar
部署指令:
-javaagent:/path/myAgent.jar
jar包规范:
在main方法之前执行
1、有Premain-Class属性 文件META-INF/MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: myagent
Premain-Class: com.thoreauz.agent.AgentMain
Implementation-Version: 1.0-SNAPSHOT
Built-By: zhaozhou
2、Premain-Class类有premain方法。
// 第一个优先级高;参数只能是一个string,如果多个参数,需要agent自己定义规范并解析
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
在main方法之后执行
同premain,只是属性和实现方法的区别
- 有Agent-Class属性
- 必须实现public的静态方法agentmain
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
Instrumentation
看premain和agentmain都有一个接收Instrumentation参数的方法,那它能干什么呢。Instrumentation是一个接口,提供了服务使得可以在运行时操作java程序,包括改变字节码,新增一个jar包,替换class等。 于是可以通过它实现各种功能的agent,比如监控,覆盖率分析,打印日志,动态部署等工具。 主要方法:
- addTransformer:可以在加载字节码时注册拦截器装换源代码
- redefineClasses: 替换class
- appendToBootstrapClassLoaderSearch: 运行时指定jar包给bootclassload加载 目前提供10多个方法,详细见api。
使用asm增强字节码
ASM库可以用来生成、转换和分析编译后的java类。asm提供了两套api,核心的API是基于事件的,而Tree API是基于对象的。基于对象模型的api构建在基于事件的模型之上。asm有如下特点:
- 小,且设计良好模块化的API,且易于使用
- 文档完善,有eclipse何idea插件帮助方便生产字节码操作api。
- 社区完善、开发。
两套api各有优缺点,基于事件的api速度更快,使用内存空间更新,但实现复杂转换较困难。下文简单介绍基于事件的api用法。
byte[] b1 = ...;
ClasssWriter cw = new ClassWriter();
ClassAdapter ca = new ClassAdapter(cw);
ClassReader cr = new ClassReader(b1);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
Reader相当于字节码生产者,把所有事件传递给writer,而adapter是一个中间适配器,形成一个转换链。下面写一个示例,往一个class的方法里面写一行代码;
public static void main(String[] args) throws Exception {
// 获取class byte
String className = "com.thoreauz.agent.Test";
InputStream resourceAsStream = asmTest.class
.getClassLoader()
.getResourceAsStream(className.replace('.', '/') + ".class");
byte[] bytes = IOUtils.toByteArray(resourceAsStream);
// 往方法里面添加代码System.out.println("hello");
ClassWriter cw = new ClassWriter(8);
TestVisitor ca = new TestVisitor(cw);
ClassReader cr = new ClassReader(bytes);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
FileOutputStream output = new FileOutputStream(new File("/tmp/target.class"));
IOUtils.write(b2, output);
}
static class TestVisitor extends ClassVisitor {
public TestVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("<init>") || mv == null) {
// 构造方法不需要添加
return mv;
}
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("hello");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
return mv;
}
}
详细文档见asm官网
示例
下面写一个javaagent,统计应用方法执行时间。 第一步: 构建agent包
public class AgentMain {
//代理程序入口函数
public static void premain(String args, Instrumentation inst) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
System.out.println("agent premain begin");
//添加字节码转换器
inst.addTransformer(new Transformer(), true);
System.out.println("agent premain end");
}
}
把AgentMain添加到jar包属性中,可以借助maven-assembly-plugin,把agent依赖都打入包中并制定premain类。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.thoreauz.agent.AgentMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
生成的jar包含了依赖的类,比如asm。
第二步: 编写Transformer
public class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className == null) {
//返回null,将会使用原生class。
return null;
}
if (className.startsWith("java") ||
className.startsWith("javax") ||
className.startsWith("jdk") ||
className.startsWith("sun") ||
className.startsWith("com/sun") ||
className.startsWith("com/intellij") ||
className.startsWith("org/jetbrains") ||
className.startsWith("com/thoreauz/agent")
) {
// 不对JDK类以及agent类增强
return null;
}
//读取类的字节码流
ClassReader reader = new ClassReader(classfileBuffer);
//创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
//接收一个ClassVisitor子类进行字节码修改
reader.accept(new TimeClassVisitor(writer, className), 8);
//返回修改后的字节码流
return writer.toByteArray();
}
}
public class TimeClassVisitor extends ClassVisitor {
private String className = null;
public TimeClassVisitor(ClassVisitor classVisitor, String className) {
super(Opcodes.ASM7, classVisitor);
this.className = className.replace('/','.');
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
//过来待修改类的构造函数
if (name.equals("<init>") || mv == null) {
// 对象初始化方法就不增强了
return mv;
}
mv = new AdviceAdapter(Opcodes.ASM7, mv, access, name, descriptor) {
@Override
public void onMethodEnter() {
//TODO 1方法进入时计时
}
@Override
public void onMethodExit(int opcode) {
//TODO 方法退出时获取结束时间并计算执行时间
}
};
return mv;
}
}
通过AdviceAdapter分别在方法进入和退出时修改方法字节码。这儿引入一个保存时间的类:
public class TimeCache {
public static Map<String, Long> startTimeMap = new HashMap<>();
public static Map<String, Long> endTimeMap = new HashMap<>();
public static void setStartTime(String methodName, long time) {
startTimeMap.put(methodName, time);
}
public static void setEndTime(String methodName, long time) {
endTimeMap.put(methodName, time);
}
public static String getCostTime(String methodName) {
long start = startTimeMap.get(methodName);
long end = endTimeMap.get(methodName);
return methodName + "[" + (end - start) + " ms]";
}
}
假设有一个test方法如下
public class Test {
public void test() {
// do something
}
}
实现计时功能,只需要把test改成如下即可:
public class Test {
public void test() {
TimeCache.setStartTime("test",System.currentTimeMillis());
// do something
TimeCache.setEndTime("test",System.currentTimeMillis());
System.out.println(TimeCache.getCostTime("test"));
}
}
于是,怎么通过asm把test方法转换成如上代码便是关键,有两种方式: 第一种:通过javap命令,反编译class为字节码结构
# javap -c Test
public class com.thoreauz.agent.Test {
public com.thoreauz.agent.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: ldc #2 // String test
2: invokestatic #3 // Method java/lang/System.currentTimeMillis:()J
5: invokestatic #4 // Method com/thoreauz/agent/TimeCache.setStartTime:(Ljava/lang/String;J)V
8: ldc #2 // String test
10: invokestatic #3 // Method java/lang/System.currentTimeMillis:()J
13: invokestatic #5 // Method com/thoreauz/agent/TimeCache.setEndTime:(Ljava/lang/String;J)V
16: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #2 // String test
21: invokestatic #7 // Method com/thoreauz/agent/TimeCache.getCostTime:(Ljava/lang/String;)Ljava/lang/String;
24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
}
对照操作指令转成asm的方法。
第一种:借助asm的ide插件直接转
intelliJ 插件 asm bytecode outline
最终得到TimeClassVisitor如下:
public class TimeClassVisitor extends ClassVisitor {
private String className = null;
public TimeClassVisitor(ClassVisitor classVisitor, String className) {
super(Opcodes.ASM7, classVisitor);
this.className = className.replace('/','.');
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
//过来待修改类的构造函数
if (name.equals("<init>") || mv == null) {
// 对象初始化方法就不增强了
return mv;
}
String key = className + ":" + name;
mv = new AdviceAdapter(Opcodes.ASM7, mv, access, name, descriptor) {
//方法进入时获取开始时间
@Override
public void onMethodEnter() {
mv.visitLdcInsn(key);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKESTATIC,
"com/thoreauz/agent/TimeCache",
"setStartTime",
"(Ljava/lang/String;J)V",
false);
}
//方法退出时获取结束时间并计算执行时间
@Override
public void onMethodExit(int opcode) {
mv.visitLdcInsn(key);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKESTATIC,
"com/thoreauz/agent/TimeCache",
"setEndTime",
"(Ljava/lang/String;J)V",
false);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(key);
mv.visitMethodInsn(INVOKESTATIC,
"com/thoreauz/agent/TimeCache",
"getCostTime",
"(Ljava/lang/String;)Ljava/lang/String;",
false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
};
return mv;
}
}
第三步:测试
public class Application {
public static void main(String[] args) throws InterruptedException {
System.out.println("hello world!");
add(1, 2);
}
private static int add(int a, int b) throws InterruptedException {
Thread.sleep(300);
return a + b;
}
}
打印出Application的main和add方法的运行时间:
# java -javaagent:/省略了path/myagent-1.0-SNAPSHOT-jar-with-dependencies.jar com.thoreauz.test.Application
agent premain begin
agent premain end
hello world!
com.thoreauz.test.Application:add[305 ms]
com.thoreauz.test.Application:main[305 ms]
总结
本文简单介绍了javaagent的使用规范,并写了premain的agent,通过asm增强代码统计方法运行时间。
javaagent的使用场景很多,agentmain在main方法运行后操作java应用更是提供了无数可能。但是真正开发agent还有许多难点比如:
- agent本身依赖asm等其他二方包,为了不污染jdk自带类和应用类,应该自定义classLoader,隔离agent类。
- asm字节码增强,需要考虑排查一些类,避免造成死循环调用。比如PrintStream.println方法注入System.out.println。还有对已经注入代码的类的判断避免重复注入(同标志或者代码锁)。
- 字节码增强已经改变了类,agent的问题代码可能影响应用的执行。
最后推荐阿里开源的两个工具,基于本文提到的原理开发而来。