java的agent初体验

之前在用到arthas就想到过可以研究下java的agent,这里算是个初入门
首先我们有个应用,需要挂上agent来探测一些事情
比如就是简单的主方法

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
Demo.start();
Demo.start2();
}
}

然后有个Demo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo {
public static void start() {
try {
System.out.println("Demo starting ...");
Thread.sleep(123);
} catch (InterruptedException ignore) {
}
}

public static void start2() {
try {
System.out.println("Demo starting 2...");
Thread.sleep(567);
} catch (InterruptedException ignore) {
}
}
}

然后我们想要像切面一样在start方法前后织入一些前后置的逻辑
我们需要将一个maven的module
然后agent代码是这样, premain 方法会在我们指定挂载agent执行,并且就是入口方法

1
2
3
4
5
6
7
8
9
public class DemoAgent {

public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("agent start");
System.out.println("arg is " + arg);
instrumentation.addTransformer(new DemoTransformer());
}

}

然后这个 transformer 就是我要怎么在目标类里如何织入代码的逻辑

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
public class DemoTransformer implements ClassFileTransformer {
// 这里是我们要织入代码的目标类
private static final String INJECTED_CLASS = "org.example.Demo";

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
// 匹配到目标类
if (realClassName.equals(INJECTED_CLASS)) {
System.out.println("injected class :" + realClassName);
CtClass ctClass;
try {
// 使用javassist,获取字节码类
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);

// 获取目标类所有的方法,也可指定方法,进行增强
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod declaredMethod : declaredMethods) {
System.out.println(declaredMethod.getName() + "method be inject");
declaredMethod.addLocalVariable("time", CtClass.longType);
declaredMethod.insertBefore("System.out.println(\"--- start ---\");");
declaredMethod.insertBefore("time = System.currentTimeMillis();");
declaredMethod.insertAfter("System.out.println(\"--- end ---\");");
declaredMethod.insertAfter("System.out.println(\"cost: \" + (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();
} catch (Throwable ignore) {
}
}
return classfileBuffer;
}
}

接下去要在pom.xml里配置如何打包agent的命令

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>org.example.DemoAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

需要指定 Premain-Class 来指定入口方法所在的类
然后用 mvn package 进行打包
打包后在我们的目标代码的启动vm参数里加上

1
-javaagent:/{path}/demo-agent-1.0-SNAPSHOT.jar

{path} 需要替换成agent的jar包所在路径
然后执行目标main方法

就可以看到结果了,这样我们第一个agent的demo就跑起来了