测试团队会做java agent的事,实现测试模拟,各种数据采集等等工作,而这些不需要开发改代码来做到,只需要挂载下agent。
目录
- javaagent认识和例子代码
- 例子:
- java.lang.instrument
- 自定义实现一个javaagent
- agent jar测试
- 回顾javaagent的作用
- 项目中用到agent的例子
javaagent认识和例子代码
例子:
java -javaagent:/Users/mubi/git_workspace/common1/common-test/target/common-test-1.0-SNAPSHOT-jar-with-dependencies.jar com.test.AgentTest
不改变代码,而是在java命令后加入-javaagent:xx.jar
实现对class的干预
java.lang.instrument
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
Provides services that allow Java programming language agents to instrument programs running on the JVM.
即在Java编程语言中,允许Java代理(Agents)进行代码插装(Instrumentation)的服务主要由Java Agent API提供。这个API允许开发者在运行时动态修改或增强Java应用程序的字节码,而无需修改源代码或重新编译。这对于性能监控、调试、安全增强等场景非常有用。
自定义实现一个javaagent
- MyAgent
package com.test;
import java.lang.instrument.Instrumentation;
/**
* @Author mubi
* @Date 2025/2/11 21:45
*/
public class MyAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs agentArgs 是我们启动 Java Agent 时带进来的参数,比如-javaagent:xxx.jar agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
customLogic(inst);
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
customLogic(inst);
}
/**
* 打印所有已加载的类名称 修改字节码
*
* @param inst
*/
private static void customLogic(Instrumentation inst) {
inst.addTransformer(new MyTransformer(), true);
}
}
- 使用javaassist改变class文件
package com.test;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
/**
* javassist 官方文档:http://www.javassist.org/tutorial/tutorial.html
*/
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// System.out.println("正在加载类:" + className);
if (!"com/test/Hello".equals(className)) {
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = cl.getDeclaredMethod("test");
System.out.println("获取方法名称:" + ctMethod.getName());
// 声明本地变量
ctMethod.addLocalVariable("start", CtClass.longType);
ctMethod.addLocalVariable("end", CtClass.longType);
ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");");
ctMethod.insertBefore("start = System.currentTimeMillis();");
// $_在Javassist中是一个特殊的变量,代表当前正在处理的类或方法中的表达式
ctMethod.insertAfter("System.out.println($_);");
ctMethod.insertAfter("end = System.currentTimeMillis();");
// 输出耗时
ctMethod.insertAfter("System.out.println((end-start) + \" ms\");");
byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
- pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common1</artifactId>
<groupId>com.container</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-test</artifactId>
<repositories>
</repositories>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<id>make-assembly</id>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.test.MyAgent</Premain-Class>
<Agent-Class>com.test.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<spring.verson>5.1.3.RELEASE</spring.verson>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
即加入:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<id>make-assembly</id>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.test.MyAgent</Premain-Class>
<Agent-Class>com.test.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
按照要求:
An agent JAR file may have both the Premain-Class and Agent-Class attributes present in the manifest. When the agent is started on the command-line using the -javaagent option then the Premain-Class attribute specifies the name of the agent class and the Agent-Class attribute is ignored. Similarly, if the agent is started sometime after the VM has started, then the Agent-Class attribute specifies the name of the agent class (the value of Premain-Class attribute is ignored).
一个代理 JAR 文件可能在清单中同时具有 Premain-Class 和 Agent-Class 属性。 当使用 -javaagent 选项在命令行上启动代理时,Premain-Class 属性指定代理类的名称,而 Agent-Class 属性将被忽略。 同样,如果代理在 VM 启动后的某个时间启动,则 Agent-Class 属性指定代理类的名称(忽略 Premain-Class 属性的值)
打包生成agent jar
agent jar测试
定义Hello类
package com.test;
import java.util.concurrent.TimeUnit;
/**
* @Author mubi
* @Date 2025/2/11 23:20
*/
public class Hello {
public String test() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("hello logic");
} catch (Exception e) {
}
return "hello test";
}
}
测试类并加上agent参数-javaagent:/Users/mubi/git_workspace/common1/common-test/target/common-test-1.0-SNAPSHOT-jar-with-dependencies.jar
测试输出如下:
即完成了代理行为。
回顾javaagent的作用
JVM启动后,JVM会执行指定Agent类的premain()方法,在premain()中可以调用Instrumentation对象的addTransformer方法注册ClassFileTransformer。当JVM加载类时会将类文件的字节数组传递给ClassFileTransformer的transform方法,在transform方法中对Class文件进行解析和修改,之后JVM就会加载转换后的Class文件。
这样实例化后执行代理了的方法就能完成,因为class文件被改过且加载好了。
项目中用到agent的例子
当一个入口流程开始后,可能因为不同的输入,执行了不同的链路。业务如果有扩展,那么就需要是否有执行,以及执行情况。
如下 AppProcess的entry方法 的入参不同,调用的扩展点实现也是不同,通过java agent 可以采集到信息
- 主程序
package com.pro.biz;
import java.util.ArrayList;
import java.util.List;
public class AppProcess {
/**
* 扩展点
*/
List<StrService> strServiceList;
public AppProcess() {
strServiceList = new ArrayList<>();
}
public void setStrServiceList(){
strServiceList.add(new X());
strServiceList.add(new Y());
}
public void entry(String name) {
String s = null;
if (name.equals("one")) {
s = strServiceList.get(0).dealStr(name);
System.out.println("entry:" + s);
return;
}
if (name.equals("all")) {
for (StrService strService : strServiceList) {
s = strService.dealStr(name);
System.out.println("entry:" + s);
}
return;
}
}
}
-
扩展点和实现类
-
Agent类
package com.pro.agent;
import java.lang.instrument.Instrumentation;
/**
* @Author mubi
* @Date 2025/2/12 22:54
*/
public class StatAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs agentArgs 是我们启动 Java Agent 时带进来的参数,比如-javaagent:xxx.jar agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
// System.out.println("premain");
customLogic(inst);
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
// System.out.println("agentmain");
customLogic(inst);
}
/**
* 打印所有已加载的类名称 修改字节码
*
* @param inst
*/
private static void customLogic(Instrumentation inst) {
inst.addTransformer(new StatTransformer(), true);
}
}
package com.pro.agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
* @Author mubi
* @Date 2025/2/12 22:55
*/
public class StatTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// if (!"com/pro/biz/AppProcess".equals(className)) {
// return classfileBuffer;
// }
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = null;
try{
ctMethod = cl.getDeclaredMethod("dealStr");
}catch (Exception e){
}
if(ctMethod != null && !className.equals("com/pro/biz/StrService")) {
// System.out.println("获取到方法名称:" + ctMethod.getName());
// System.out.println(className);
// ctMethod.insertBefore("System.out.println(\"入参: \" + $1);");
// ctMethod.insertAfter("System.out.println(\"出参: \" + $_);");
ctMethod.insertAfter(
"com.pro.stat.StrServiceCallStats.addCallInfo($0.getClass().getName(), $1, $_);"
);
// StringBuilder methodBody = new StringBuilder();
// methodBody.append("{")
// .append("System.out.println(\"Input: \" + $1);") // input parameter
// .append("$r = $proceed($$);") // proceed with the original method
.append("System.out.println(\"Output: \" + result);") // output
// .append("return $r;")
// .append("}");
//
// // Modify the method
// ctMethod.setBody(methodBody.toString());
}
// 插入统计代码
// String insertBeforeCode = "{ $_ = $proceed($$); " +
// "StrServiceCallStats.addCallInfo($0.strServiceList.get($index).getClass().getName(), $1, $_); }";
// entryMethod.insertBefore(insertBeforeCode);
byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
- pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common1</artifactId>
<groupId>com.container</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-project</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<id>make-assembly</id>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.pro.agent.StatAgent</Premain-Class>
<Agent-Class>com.pro.agent.StatAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<spring.verson>5.1.3.RELEASE</spring.verson>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.0-GA</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
打包后生成agent jar, 运行时加上-javaagent:/Users/mubi/git_workspace/common1/common-project/target/common-project-1.0-SNAPSHOT-jar-with-dependencies.jar