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项目)类的值和行为。


文章作者: Cheney
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cheney !
 上一篇
开源利器:自动生成随机 mock 数据测试对象 开源利器:自动生成随机 mock 数据测试对象
测试的痛点大家好,我是老马。 每一位开发者大部分工作都是写代码、测试代码、修BUG。 我们有很多测试代码,总是花费大量的实践去构建一个对象。 于是就在想,能不能自动填充一个对象呢? 于是去 github 查了一下,找到了一个测试神器 dat
2020-12-25
下一篇 
常用的加密算法 常用的加密算法
加密算法我们整体可以分为:可逆加密和不可逆加密,可逆加密又可以分为:对称加密和非对称加密。 一、不可逆加密常见的不可逆加密算法有MD5,HMAC,SHA1、SHA-224、SHA-256、SHA-384,和SHA-512,其中SHA-224
2020-12-17
  目录