Java Agent 独立于应用程序的代理程序

图怪兽_5e294cd0b382b53bd1f6f4ad195b4095_46939.png

背景

Java Agent是一种特殊类型的类,通过使用Java Instrumentation API,可以拦截在JVM上运行的应用程序,修改它们的字节码。Java代理非常强大,但也很危险。

简介

image.png
Java Agent 是基于工具的,来自Java平台,存在于Java.lang工具包,它提供了允许代理检测运行在JVM上的程序的服务。这个包非常简单且自包含,因为它包含一对异常类、一个数据类、类定义和两个接口。在这两个接口中,如果我们想要编写Java代理,我们只需要实现classFileTransformer接口。

两种实现方法

静态代理

这意味着我们构建我们的代理,我们将它打包为一个jar文件,当我们启动我们的Java应用程序时,我们传入一个特殊的JVM参数,称为javaagent。然后,我们给它提供代理jar文件在磁盘上的位置。

1.创建一个agent的项目
1.1 创建agent类

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
public class JavaAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
log.info("execute premain args:{}",agentArgs);
ClassFileTransformer interceptingClassTransformer = (loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
log.info("ClassLoader:{} className:{}",loader,className);
return classfileBuffer;
};
instrumentation.addTransformer(interceptingClassTransformer,true);
}
}

1.2 创建MATE-INF/MANIFEST.MF文件

1
2
3
4
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: io.demo.agent.JavaAgent

1.3 配置pom文件

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>io.demo.agent.JavaAgent</Premain-Class>
<Agent-Class>io.demo.agent.JavaAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>

1.4 将agent项目打成jar包

1
mvm clean package

2.创建另一个可运行的项目execute

2.1创建一个可运行的类

1
2
3
4
5
6
@Slf4j
public class Execute {
public static void main(String[] args) {
log.info("i am execute main");
}
}

2.2 配置pom文件

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!--这里写你的main函数所在的类的路径名,也就是Class.forName的那个字符串-->
<mainClass>io.demo.execute.Execute</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

2.3 运行命令

1
java -javaagent:agent-1.0-SNAPSHOT.jar -jar execute-1.0-SNAPSHOT.jar

2.4 查看输出
image.png

动态代理

与启动应用程序的方式不同,您可以编写一小段代码,获取并连接到现有的JVM,并告诉它加载某个代理。

1
2
3
4
5
6
7
8
9
10
11
public class DynamicLoadAgent {
public static final String AGENT_FILE_PATH = "/Users/biaoyang/IdeaProjects/java-agent/agent/target/agent-1.0-SNAPSHOT.jar";
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
//通过pid连接进入指定java程序jvm
VirtualMachine vm = VirtualMachine.attach("4895");//pid可通过jps查看或VirtualMachine.list()
//加载agent
vm.loadAgent(AGENT_FILE_PATH);
//脱离虚拟机
vm.detach();
}
}

这个参数agentFilePath与静态代理方法中的参数完全相同。它必须是agent jar的文件名,因此没有输入流就没有字节。对于这种方法有两个注意事项。第一个这是sun包下的私有API,通常适用于热点实现。第二个是,用java 9,你不能再使用这些代码来连接它运行的JVM。

详解

Premain方法有两个参数:

  • agentArgs:字符串参数,用户选择将其作为参数传递给Java代理调用。
  • Instrumentation:来自java.lang包,我们可以添加一个新的ClassFileTransformer对象,它包含我们的代理的实际逻辑。
    1
    2
    3
    4
    5
    6
    7
    8
    //会在用户类加载前先执行
    //优先选择这个方法执行
    public static void premain(String agentArgs, Instrumentation instrumentation)
    public static void premain(String agentArgs)
    //JDK1.6以后新增的,可以实现在main方法执行以后进行插入执行。
    public static void agentmain (String agentArgs, Instrumentation inst)
    public static void agentmain (String agentArgs)

案例

通过javassist实现修改字节码的操作。

1.编写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
@Slf4j
public class JavaAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
log.info("execute premain args:{}", agentArgs);
ClassFileTransformer interceptingClassTransformer = (loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {

log.info("ClassLoader:{} className:{}", loader, className);
if ("io/demo/execute/Execute".equals(className)) {
try {
// 从ClassPool获得CtClass对象
log.info("aaaaaaaa");
final ClassPool classPool = ClassPool.getDefault();
log.info("ClassPool={}", classPool);
final CtClass clazz = classPool.get("io.demo.execute.Execute");
log.info("clazz={}", clazz);
CtMethod convertToAbbr = clazz.getDeclaredMethod("getInfo");
// 覆盖方法体
String methodBody = "{return \"aget update\";}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//从ClassPool中移除这个CtClass对象
clazz.detach();
return byteCode;
} catch (Throwable ex) {
ex.printStackTrace();
}
}
return null;
};
instrumentation.addTransformer(interceptingClassTransformer);
}
}

2.执行的类

1
2
3
4
5
6
7
8
9
10
@Slf4j
public class Execute {
public static void main(String[] args) {
log.info("i am execute main");
log.error(getInfo());
}
public static String getInfo(){
return "Execute getInfo";
}
}

3.执行

1
java -javaagent:agent-1.0-SNAPSHOT.jar -jar execute-1.0-SNAPSHOT.jar

4.执行结果
image.png

资料

什么是 Java Agent ?: https://www.developer.com/design/what-is-java-agent/

Understanding Java Agents:https://dzone.com/articles/java-agent-1


Java Agent 独立于应用程序的代理程序
https://mikeygithub.github.io/2021/12/29/yuque/笔记篇-Java Agent 独立于应用程序的代理程序/
作者
Mikey
发布于
2021年12月29日
许可协议