0%

javaagent-premain使用介绍

前言

javaagent是jvm提供的一个“插件”,通过在javaagent可以在不侵入源码的情况下在运行时动态修改java类。本文将介绍如何通过启动时加载 javaagent,利用premain的方式在不修改源码的情况下将部分函数功能进行替换或修改。

基础类介绍

本例中要修改的函数是 TestUser 中的 toStringgetName,其中,toString 是整体替换,getName 是在函数执行前后分别打印一条日志。TestUser 基础类如下

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
package com.guozhchun.javaagent;

public class TestUser {
private String id;

private String name;

public TestUser(String id, String name) {
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public String getName() {
System.out.println("get name in TestUser");
return name;
}

@Override
public String toString() {
return "TestUser{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}

premain 函数

javaagent支持在启动时进行加载,此时需要实现以下的方法。JVM会首先寻找第一个带 Instrumentation 参数的方法,如果没有发现,再寻找第二个不带 Instrumentation的方法。

1
2
public static void premain(String args, Instrumentation inst);
public static void premain(String args);

javaagent 也支持在运行过程中动态加载,此时则需要实现以下的方法。同样的,JVM会首先寻找第一个带 Instrumentation 参数的方法,如果没有发现,再寻找第二个不带 Instrumentation的方法。

1
2
public static void agentmain(String args, Instrumentation inst);
public static void agentmain(String args);

这几个方法的第一个参数 args 是随同 -javaagent 一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。参数传递方式如 -javaagent xxx.jar=key1=value1,key2=value2,此时args 获取到的值是 key1=value1,key2=value2instInstrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

本例将使用启动时加载javaagent的方式进行功能的增强和修改。

首先,定义 premain 函数,并在函数中增加 ClassFileTransformer 转换类,这个类是用来增强待修改类和函数的。此类主要实现两个功能:

  • 重写 TestUser 类中 toString 方法,替换原有函数体
  • 修改 TestUser 类中 getName 方法,在其函数体前后增加打印日志的操作

本例中,用到了 javassist 库提供的函数来进行类方法的修改。其 maven 坐标如下

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.1-GA</version>
</dependency>

premain 的代码如下

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
package com.guozhchun.javaagent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.Instrumentation;

public class TestAgentPreMain {
public static void premain(String args, Instrumentation inst) {
System.out.println("-------premain, args: " + args);
inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
if (!className.equals("com/guozhchun/javaagent/TestUser")) {
return classfileBuffer;
}

try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass(className.replace("/", "."));
CtMethod method = ctClass.getDeclaredMethod("toString");
String body = "{";
body += "System.out.println(\"Change in transformer: call toString\");";
body += "return \"id: \" + id + \", name: \" + name;";
body += "}";
method.setBody(body);

CtMethod ctMethod = ctClass.getDeclaredMethod("getName");
ctMethod.insertBefore("System.out.println(\"Change in transformer: Before get name\");");
ctMethod.insertAfter("System.out.println(\"Change in transformer: After get name\");");

byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}

return classfileBuffer;
});
}
}

MANIFEST.MF

要使用 javaagent,还需要定义 MANIFEST.MF 文件,在文件中指明 Premain-Class,如本例中 Premain-Class: com.guozhchun.javaagent.TestAgentPreMain。同时需要将这个文件打包在 jar 中。

对于使用 maven 打包的方式,可以使用以下定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.guozhchun.javaagent.TestAgentPreMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>

打出来的 MANIFEST.MF 文件如下

1
2
3
4
5
6
Manifest-Version: 1.0
Premain-Class: com.guozhchun.javaagent.TestAgentPreMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Build-Jdk-Spec: 1.8
Created-By: Maven JAR Plugin 3.2.2

main 函数

编写 main 函数,调用 TestUser 中的方法

1
2
3
4
5
6
7
8
9
10
11
package com.guozhchun.javaagent;

public class TestAgent {
public static void main(String[] args) {
System.out.println("------------Test javaagent-------------");
TestUser testUser = new TestUser("1", "alice");
System.out.println("get user info: " + testUser);
System.out.println("get user id: " + testUser.getId());
System.out.println("get user name: " + testUser.getName());
}
}

运行 javaagent

premain 所在类及相关类打成 jar 包,运行 TestAgent main 方法,传入 javaagent 参数,java -javaagent:xxx -jar xxx。运行结果如下

1
2
3
4
5
6
7
8
9
-------premain, args: null
------------Test javaagent-------------
Change in transformer: call toString
get user info: id: 1, name: alice
get user id: 1
Change in transformer: Before get name
get name in TestUser
Change in transformer: After get name
get user name: alice

可以看到,在执行 main 函数前,会先执行 premain 方法。同时在调用 TestUser 的相关方法时,也不是 TestUser 类中的默认实现,而是 premain 方法中增加的 ClassFileTransformer 类里修改过的函数体代码。从这里可以看出,javaagent确实是可以在不修改源码的前提下对部分类和函数进行功能的修改。

参考资料

  1. https://zhuanlan.zhihu.com/p/510702981
  2. https://blog.csdn.net/WTUDAN/article/details/120573235