文章目录
- 简介
- agent与attach
- agent
- attach
- 如何attach
- pom.xml
简介
javaagent是什么?
从名字agent也可以看出,是一种代理。
javaagent用来做什么?
本质上是对class的一种增强,用来实现一些通用功能,例如链路追踪等。
和AOP有什么区别?
AOP和javaagent本质上都是通过修改class来实现额外功能,对代码逻辑本身无侵入,在运行时侵入。
AOP通常是项目内的代理增强,通常是增强业务逻辑,例如:公用授权检查逻辑。
javaagent是项目外独立的增强项目,通常是非业务逻辑,例如:arthas相关功能、debug、线上运行参数、返回值等数据临时打印等。
javaagent可能我们基本不会用,但是我们最好理解其原理,知道它能做什么,这样我们可以更好理解jacoco、arthas这些工具的原理。
可以丰富工具箱,在我们自己要做项目的时候,也有更多的工具可供选择。
本文重点介绍流程,具体的逻辑本质上还是对字节码的操作,可以看asm、javaassist、cglib、bytebuddy等字节码操作工具。
agent与attach
agent是在启动的时候就指定,在类加载到jvm之前就完成了类的增强,如jacoco
attach可以对已经启动项目,已经加载到jvm中的类进行增强,例如arthas。(这点非常有用,可以不用重启项目,甚至可以做热更新)
agent
首先,我们准备一个需要增强的类,简单点:
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class Start {
public static final Random random = new Random();
public static void main(String[] args) throws InterruptedException {
System.out.println("Start开始执行...");
while (true) {
doBusiness();
}
}
public static void doBusiness() throws InterruptedException {
System.out.println("doBusiness 执行开始");
int time = random.nextInt(10) + 1;
TimeUnit.SECONDS.sleep(time);
}
}
就一个doBusiness业务方法,用来增强。
agent需要一个静态的premain方法,方法签名如下:
public static void premain(String arg, Instrumentation instrumentation)
这个方法在哪个类中不重要,重要的是方法签名要一样。
import vip.meet.transformer.LogTransformer;
import java.lang.instrument.Instrumentation;
public class PreMain {
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("执行premain 方法");
System.out.println("执行premain参数:" + arg);
instrumentation.addTransformer(new LogTransformer());
}
}
LogTransformer类使用了javaassist,依赖看后面的pom文件
transform方法是在jvm加载类之前执行。
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 LogTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replaceAll("/", ".");
if (realClassName.equals("vip.meet.Start")) {
CtClass ctClass;
try {
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
CtMethod ctMethod = ctClass.getDeclaredMethod("doBusiness");
ctMethod.addLocalVariable("inject_start", CtClass.longType);
ctMethod.insertBefore("System.out.println(\"---doBusiness agent 开始执行---\");");
ctMethod.insertBefore("inject_start = System.currentTimeMillis();");
ctMethod.insertAfter("System.out.println(\"---doBusiness agent 结束执行---\");");
ctMethod.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - inject_start));");
return ctClass.toBytecode();
} catch (Throwable e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
return classfileBuffer;
}
}
javaagent如何知道代理类入口呢?
答案是MANIFEST.MF的Premain-Class:
Premain-Class: vip.meet.agent.PreMain
可以在maven-jar-plugin、maven-assembly-plugin插件中配置,参考后面pom文件配置。
mvn clean package
java -javaagent:agent-learn-1.0.0-jar-with-dependencies.jar=hello,abc=123 -jar agent-learn-1.0.0-jar-with-dependencies.jar
attach
attach的和agent非常相似,只是入口不一样。
attach方式的入口方法签名如下:
public static void agentmain(String arg, Instrumentation instrumentation)
attach通常就不使用ClassFileTransformer,因为这个是jvm加载类之前的调用。
而attach的时候,jvm已经启动了。
所以,我们需要获取已经加载的类:
Class<?>[] classes = instrumentation.getAllLoadedClasses();
修改类之后,再redefineClasses:
ClassDefinition classDefinition = new ClassDefinition(cls, ctClass.toBytecode());
instrumentation.redefineClasses(classDefinition);
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static void agentmain(String arg, Instrumentation instrumentation) {
System.out.println("agentmain启动");
System.out.println("agentmain参数:" + arg);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
for (Class<?> cls : classes) {
String name = cls.getName();
if (name.equals("vip.meet.Start")) {
CtClass ctClass;
try {
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(name);
CtMethod ctMethod = ctClass.getDeclaredMethod("doBusiness");
ctMethod.addLocalVariable("inject_start", CtClass.longType);
ctMethod.insertBefore("System.out.println(\"---doBusiness agent 开始执行---\");");
ctMethod.insertBefore("inject_start = System.currentTimeMillis();");
ctMethod.insertAfter("System.out.println(\"---doBusiness agent 结束执行---\");");
ctMethod.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - inject_start));");
ClassDefinition classDefinition = new ClassDefinition(cls, ctClass.toBytecode());
instrumentation.redefineClasses(classDefinition);
} catch (Throwable e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
}
}
如何配置attach入口呢?
答案是MANIFEST.MF的Agent-Class:
Agent-Class: vip.meet.attach.AgentMain
如何attach
现在,我们有attach了,如何attach到已经运行的jvm进程上呢?
可以通过VirtualMachine来实现,注意VirtualMachine是sun的私有实现接口,依赖tools.jar
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AttachUseMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, InterruptedException {
System.out.println("AttachUseMain启动");
for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
String name = vmd.displayName();
System.out.println(name);
if (name.equals("agent-learn-1.0.0-jar-with-dependencies.jar")) {
VirtualMachine vm = VirtualMachine.attach(vmd.id());
vm.loadAgent("E:\\app\\me\\learn\\agent-learn\\target\\agent-learn-1.0.0-jar-with-dependencies.jar=hello,ok,aa,bb,cc=3");
TimeUnit.MINUTES.sleep(1);
vm.detach();
}
}
}
}
上面的代码,首先列出所有jvm进程,然后匹配到需要attach的pid,然后执行attach。
首先运行,需要被代理的项目:
java -jar agent-learn-1.0.0-jar-with-dependencies.jar
然后启动AttachUseMain:
注意:项目运行的java的版本和AttachUseMain一样,否则会出错:Non-numeric value found - int expected
其实jstack、jmap都是通过attach方式实现,不过没有使用jar包,而是通过JVMTI的其他接口实现。
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vip.meet</groupId>
<artifactId>agent-learn</artifactId>
<version>1.0.0</version>
<name>agent-learn</name>
<description>agent-learn</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>D:/Env/JDK/Java8/lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>vip.meet.Start</mainClass>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>vip.meet.agent.PreMain</Premain-Class>
<Agent-Class>vip.meet.attach.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>