java中agent使用
一、简介
java agent是独立于应用程序外的代理程序,可以在应用程序启动前或运行中,修改类字节码信息,改变类的行为。这里对应用程序启动前和运行中的agent使用分别介绍。
二、应用程序启动前的agent使用
应用程序启动前agent使用,是通过在应用程序启动时添加-javaagent参数(可多个-javaagent参数)实现的。
2.1 javaagent参数格式
javaagent参数使用格式如下:
java -javaagent:/xx/agent.jar[=参数] -jar xx.jar
2.2 开发步骤
应用程序启动前agent开发,包含agent程序开发、MENIFEST.MF配置文件定义、maven中maven-jar-plugin插件修改、打jar包、主程序调用,具体开发步骤如下:
定义agent程序,需包含方法名为premain的静态方法,同时实现ClassFileTransformer接口对特定类字节码修改(结合javassist工具);
定义MENIFEST.MF配置文件,位于resources/META-INF目录下,内容类似:
Manifest-Version: 1.0.1 Premain-Class: com.dragon.study.spring_boot_pre_agent.PreAgentMain Can-Redefine-Classes: true其中Premain-Class为前面定义的agent类,且配置文件最要空一行。
修改mava的pom.xml中的插件配置,类似于:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class> com.dragon.study.spring_boot_pre_agent.PreAgentMain </Premain-Class> </manifestEntries> </archive> </configuration> </plugin>将前面定义的agent程序打成jar包;
在目标主程序上,添加-javaagent参数及前面的agent的jar包,再运行目标主程序,类似于:
java -javaagent:/xx/agent.jar=agentArgs -cp /xx.jar xx.Main
2.3 示例
这是以spring_boot_pre_agent项目创建agent的jar包,以spring_boot_main项目中的PreAgentTargetMain为目标类为例。
2.3.1 agent项目spring_boot_pre_agent
2.3.1.1 agent项目中的maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dragon.study</groupId>
<artifactId>spring_boot_pre_agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_boot_pre_agent</name>
<description>Demo project for Spring Boot</description>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
com.dragon.study.spring_boot_pre_agent.PreAgentMain
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3.1.2 agent项目中的agent相关类
这里agent相关类包含自定义字节码编辑类ConfigTransformer.java和agent主类PreAgentMain.java,如下:
ConfigTransformer.java类如下:
package com.dragon.study.spring_boot_pre_agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
//修改类的字节码
public class ConfigTransformer implements ClassFileTransformer {
private static ClassPool classPool = ClassPool.getDefault();
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String target = "com.dragon.study.spring_boot_main.StaticConfig";
className = className.replaceAll("/", ".");
if(className.contains(target)){
try {
CtClass ctClass = classPool.getCtClass(target);
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
//指定方法添加一行自定义输出
ctMethod.insertBefore("System.out.println(\"pre inject, configName:\"+configName);");
//返回修改后的字节码
return ctClass.toBytecode();
} catch (Exception e) {
}
}
//返回原类字节码
return classfileBuffer;
}
}
PreAgentMain类如下:
package com.dragon.study.spring_boot_pre_agent;
import java.lang.instrument.Instrumentation;
public class PreAgentMain {
//主程序运行前执行自定义操作
public static void premain(String agentArgs, Instrumentation inst) {
//这里示例打印传入参数
System.out.println("agentArgs:"+agentArgs);
//修改指定类行为
inst.addTransformer(new ConfigTransformer());
}
}
2.3.1.3 agent项目中的MENIFEST.MF配置文件
MENIFEST.MF配置文件位于resources/META-INF目录下,内容为:
Manifest-Version: 1.0.1
Premain-Class: com.dragon.study.spring_boot_pre_agent.PreAgentMain
Can-Redefine-Classes: true
2.3.1.4 agent项目打为jar包
这里通过maven打jar包为:spring_boot_pre_agent-0.0.1-SNAPSHOT.jar
2.3.2 agent目标项目spring_boot_main
spring_boot_main示例包含目标相关类,以及最终添加agent启动。
2.3.2.1 目标相关类
目标相关类包含静态配置类StaticConfig.java和目标类PreAgentTargetMain.java如下:
StaticConfig.java:
package com.dragon.study.spring_boot_main;
public class StaticConfig {
//自定义静态属性
public static String configName="apple";
//自定义静态方法
public static String sayHello(){
return "hello " + configName;
}
}
PreAgentTargetMain.java:
package com.dragon.study.spring_boot_main.agent;
import com.dragon.study.spring_boot_main.StaticConfig;
public class PreAgentTargetMain {
public static void main(String[] args) {
System.out.println("main");
//调用指定方法
StaticConfig.sayHello();
}
}
2.3.3 测试
应用程序启动前使用agent,是通过启动时添加-javaagent参数实现的。测试调用如下:
java -javaagent:/xx/spring_boot_pre_agent-0.0.1-SNAPSHOT.jar=agentArgs -cp /xx/spring_boot_main-0.0.1-SNAPSHOT.jar com.dragon.study.spring_boot_main.agent.PreAgentTargetMain
输出:
agentArgs:agentArgs
main
pre inject, configName:apple
总结分析,借助应用程序启动前agent的使用,在PreAgentTargetMain启动前改变了其行为。
三、应用程序运行中的agent使用
应用程序运行中的agent使用,是通过第三方程序,借助VirtualMachine将自定义agent添加到目标程序(通过进程号pid)上。
3.1 开发步骤
应用程序运行中的agent开发和运行前类似,只是启动方式不同,包含agent程序开发、MENIFEST.MF配置文件定义、maven中maven-jar-plugin插件修改、打jar包、agent使用的目标程序、第三方启动程序,具体开发步骤如下:
定义agent程序,需包含方法名为agentmain的静态方法,同时实现ClassFileTransformer接口对特定类字节码修改(结合javassist工具);
定义MENIFEST.MF配置文件,位于resources/META-INF目录下,内容类似:
Manifest-Version: 1.0.1 Agent-Class: com.dragon.study.spring_boot_post_agent.PostAgentMain Can-Retransform-Classes: true Can-Redefine-Classes: true其中Agent-Class为前面定义的agent类,且配置文件最要空一行。
修改mava的pom.xml中的插件配置,类似于:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class> com.dragon.study.spring_boot_post_agent.PostAgentMain </Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>将前面定义的agent程序打成jar包;
通过第三方程序将agent程序添加到目标程序上,类似:
//获取指定项目运行的pid String targetPid = "xx" //运行期,对指定pid程序添加agent,动态改变程序行为 VirtualMachine vm = VirtualMachine.attach(targetPid); //添加指定agent的jar包 vm.loadAgent("/xx/agent.jar"); vm.detach();
3.2 示例
这是以spring_boot_post_agent项目创建agent的jar包,以spring_boot_main项目为目标项目,以PostAgentTargetMain.java为第三方添加程序为例。
3.2.1 agent项目spring_boot_post_agent
3.2.1.1 agent项目中的maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dragon.study</groupId>
<artifactId>spring_boot_post_agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_boot_post_agent</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>
com.dragon.study.spring_boot_post_agent.PostAgentMain
</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2.1.2 agent项目中的agent相关类
这里agent相关类包含自定义字节码编辑类ConfigTransformer.java和agent主类PreAgentMain.java,如下:
ConfigTransformer.java类如下:
package com.dragon.study.spring_boot_post_agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Loader;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ConfigTransformer implements ClassFileTransformer {
private static ClassPool classPool = ClassPool.getDefault();
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String target = "com.dragon.study.spring_boot_main.StaticConfig";
className = className.replaceAll("/", ".");
if(className.contains(target)){
try {
CtClass ctClass = classPool.getCtClass(target);
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
//指定方法添加一行自定义输出
ctMethod.insertBefore("System.out.println(\"post inject, configName:\"+configName);");
//返回修改后的字节码
return ctClass.toBytecode();
} catch (Exception e) {
}
}
//返回原类字节码
return classfileBuffer;
}
}
PostAgentMain类如下:
package com.dragon.study.spring_boot_post_agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.stream.Stream;
public class PostAgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) {
//这里示例打印传入参数
System.out.println("agentArgs:" + agentArgs);
//打印加载的所有类
Class<?>[] clazzArr = inst.getAllLoadedClasses();
// Stream.of(clazzArr).forEach(System.out::println);
//打印目标项目中指定字段的内存值
Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
try {
System.out.println("configName:"+t.getDeclaredField("configName").get(null));
} catch (Exception e) {
e.printStackTrace();
}
});
//修改目标项目中指定字段的内存值
Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
try {
t.getDeclaredField("configName").set(null, "banana");
} catch (Exception e) {
e.printStackTrace();
}
});
//运行期修改指定类行为
Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
try {
inst.addTransformer(new ConfigTransformer(), true);
inst.retransformClasses(t);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
});
}
}
3.2.1.3 agent项目中的MENIFEST.MF配置文件
MENIFEST.MF配置文件位于resources/META-INF目录下,内容为:
Manifest-Version: 1.0.1
Agent-Class: com.dragon.study.spring_boot_post_agent.PostAgentMain
Can-Retransform-Classes: true
Can-Redefine-Classes: true
3.2.1.4 agent项目打为jar包
这里通过maven打jar包为:spring_boot_post_agent-0.0.1-SNAPSHOT.jar
3.2.2 agent目标项目spring_boot_main
spring_boot_main为完整常规项目。
3.2.2.1 添加maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dragon.study</groupId>
<artifactId>spring_boot_main</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_boot_main</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2.2.2 application.yaml配置
server:
port: 10013
spring:
application:
name: spring-boot-main
3.2.2.3 关键类
目标相关类包含静态配置类StaticConfig.java和启动类:
StaticConfig.java:
package com.dragon.study.spring_boot_main;
public class StaticConfig {
//自定义静态属性
public static String configName="apple";
//自定义静态方法
public static String sayHello(){
return "hello " + configName;
}
}
启动类SpringBootMainApplication.java:
package com.dragon.study.spring_boot_main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootMainApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(SpringBootMainApplication.class, args);
//主动加载静态类
Class.forName("com.dragon.study.spring_boot_main.StaticConfig");
}
}
3.3.3 第三方添加类
第三方添加类是通过VirtualMachine将agent和目标项目关联起来,示例PostAgentTargetMain如下:
package com.dragon.study.spring_boot_main.agent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class PostAgentTargetMain {
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> vmList = VirtualMachine.list();
//获取指定项目运行的pid
String targetPid = vmList.stream().filter(t->t.displayName().endsWith("com.dragon.study.spring_boot_main.SpringBootMainApplication")).findFirst().map(t->t.id()).get();
//运行期,对指定pid程序添加agent,动态改变程序行为
VirtualMachine vm = VirtualMachine.attach(targetPid);
//添加指定agent的jar包
vm.loadAgent("/xx/spring_boot_post_agent-0.0.1-SNAPSHOT.jar");
vm.detach();
}
}
3.3.4 测试
启动spring_boot_main项目,运行第三方添加类PostAgentTargetMain。
java -javaagent:/xx/spring_boot_pre_agent-0.0.1-SNAPSHOT.jar=agentArgs -cp /xx/spring_boot_main-0.0.1-SNAPSHOT.jar com.dragon.study.spring_boot_main.agent.PreAgentTargetMain
spring_boot_main输出:
agentArgs:null
configName:apple
再次通过http查看:
GET http://localhost:10013/hello/sayHello
Accept: application/json
http结果输出:
hello banana
同时spring_boot_main输出:
post inject, configName:banana
分析总结:
分析前面测试结果,可以发现,借助运行期agent的使用,实现了动态获取、修改、新增运行期StaticConfig.java(spring_boot_main项目)类的值和行为。