javaagent

java.lang.instrument包是Java中来增强JVM上的应用的一种方式,机制是在JVM启动前或启动后attach上去进行修改方法字节码的方式。
instrument包的用途很多,主要体现在对代码侵入低的优点上,例如一些监控不方便修改业务代码,但是可以使用这种方式在方法中植入特定逻辑,这种方式能够直接修改JVM中加载的字节码的内容,而不需要在像Spring AOP实现中创建新的代理类,所以在底层侵入更高,但是对开发者更透明。用于自动添加getter/setter方法的工具lombok就使用了这一技术。另外btrace和housemd等动态诊断工具也是用了instrument技术。

instrument的使用方式通过在启动命令上添加-javaagent:xxx.jar的方式加载一个称为agent的jar包,jar包的META-INF/MANIFEST.MF中应当声明Premain-Class或Main-Class。启动时JVM会寻找这个类中的public static void premain(String agentArgs, Instrumentation instrumentation), instrumentation对象中可以添加自己的类修改逻辑进行字节码修改。另外当通过attach到一个运行中的JVM的方式时,可以调用agentmain方法来获取Instrument对象进行类的重定义。

如果我们想在一个方法的前后加上一些逻辑,也就是我们常说的AOP,可能会想到SpringAOP的方式,或者使用java.lang.Proxy,或者使用ASM,Cglib等生成子类,但是这些都多少需要对代码进行修改适配。如何在一个代码已经写好的情况下修改它的逻辑呢,本质上也是修改字节码,只不过区别在于修改的时机。

下面我用一个修改方法的例子来进行阐述。

假设有这样的一个类。

1
2
3
4
5
6
7
8
9
10
public class TestJVM {
public static void main(String[] args) {
TestJVM testJVM = new TestJVM();
testJVM.say();
}
public void say() {
System.out.println("Say Hello");
}
}

我想在say方法的前面添加一个语句,那么使用Instrument的方式是这样的。
创建一个用来生成javaagent的工程,创建META-INF/MANEFIST.MD文件,并设置Premain-Class等
javaagent-project

然后创建我们的AgentMain类,里面定义了premain和agentmain方法,方法中使用ASM对加载的类进行重新转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.lzy.javaagent;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* Description:
*
* @author liuzhengyang
* @version 1.0
* @since 2017-03-15
*/
public class AgentMain {
public static void premain(String agentOps, Instrumentation inst) {
instrument(agentOps, inst);
}
public static void agentmain(String agentOps, Instrumentation inst) {
instrument(agentOps, inst);
}
private static void instrument(String agentOps, Instrumentation inst) {
System.out.println(agentOps);
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return transformClass(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
});
System.out.println(inst);
}
private static byte[] transformClass(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("Transform loader " + loader + " className: " + className + " classBeingRedefined " + classBeingRedefined);
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classReader.accept(new MyClassClassVisitor(classWriter), 0);
byte[] bytes = classWriter.toByteArray();
return bytes;
}
static class MyClassClassVisitor extends ClassVisitor {
public MyClassClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[]
exceptions) {
System.out.println("visit " + name + " desc " + desc);
if ("say".equals(name)) {
// do call
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("CALL " + name);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitEnd();
return methodVisitor;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
}

在pom文件中添加一些plugin,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
</resource>
</resources>
</build>
</project>

使用maven clean package 便可以生成出将要使用的jar包,比如叫javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar,位于/path/to目录下
然后在执行TestJVM类时添加参数 -javaagent:/path/to/javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar

然后就可以看到方法被转化后的执行结果了。

javaagent debug

在IDE中agentmain的这种javaagent的debug和普通的Java代码是一样的。

代码

完整的代码 放在了javaagent

总结

当然,更好的使用方式是在pom中添加依赖,不需要再启动时增加参数并指定jar包位置。已经有很多项目如lombok,stagemonitor都实现了这样的功能,我会继续研究并添加进来。

参考