一、簡介
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>
</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>
</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>
</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專案)類的值和行為。