JVMTI黑科技
JVMTI黑科技
JVMTI简介
JVMTI(JVM tool interface)的简称,由原来的JVMDI(JVM debug interface)和JVMPI(JVM profile interface)合并而来。就是与JVM的直接交互的一系列接口,JVM用c/c++开发,所以,这一系列接口是c/c++的接口。通过这一系列接口,我们可以对JVM进行性能分析、debug、内存管理、线程分析等各种黑科技操作。
JavaAgent
通过c/c++去与JVM交互显然对于大多数的Java程序员而言,并不方便,所以,JVMTI针对Java语言提供了一个Instrumentation的接口,可以通过Java代码调用libinstrument的动态库与JVMTI接口进行交互。先从最简单的javaagent的两种注入方式讲解,写两个简单的例子,可以直观的感受到两种javaagent的加载方式,然后,讲解在注入的过程中,怎么样进行class的字节码增强。
Instrument
包含两种方式的整合形式,一种是main方法启动前执行,一种是main方法内部通过attach来进行加载。
- premain(Agent模式): 目标应用main方法启动前
java -javaagent:/path/to/javaagent.jar -jar application.jar
其中,-javaagent
需要在-jar的前面,如果在后面,不生效。
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
- agentmain(Attach模式): 目标应用之外,用一个attach应用将javaagent.jar注入到目标应用中
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
premain(Agent模式)
对于premain这种方式,相对比较简单,就是有一个javaagent的jar包,然后,在启动命令上把这个jar加上去之后,就会在启动main方法之前先运行这个premain方法。需要注意的是,要想使这个jar包知道启动哪一个premain方法,我们还需要在manifest文件里面进行定义。定义menifast的方法也有两种,一种是直接编写menifast文件,还有一种更推荐的是,使用maven的插件进行编写。
在pom.mxl文件中添加:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>AgentDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Main-Class>org.example.Main</Main-Class>
<Premain-Class>org.example.SimpleAgent</Premain-Class>
<Agent-Class>org.example.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
这样,其实我们就可以写一个简单的agent jar进行测试了:
javaagent:
package org.example;
import java.lang.instrument.Instrumentation;
public class SimpleAgent {
public static void premain(final String agentArgs,
final Instrumentation inst) {
System.out.println("main方法调用前会先调用该agent的premain方法!");
}
}
目标应用main:
package org.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
项目结构如下:
然后,可以编译出一个Agent的jar:
注意:这里不要使用plugins里面的jar插件去进行打包,这个插件仅打包,不会编译,会导致classes为空,报找不到的错误,或者修改了代码,但是classes还是老的,没有重新编译,要定位问题就很麻烦了。
然后,就可以使用命令行的形式启动测试,或者使用IDEA进行测试:
我们这里仅仅是为了演示,所以,主程序的Main jar 和 agent jar就合在一起了,实际项目中,通常是两个不同的jar。
- 命令行
java -javaagent:target/AgentDemo-1.0-SNAPSHOT.jar -jar target/AgentDemo-1.0-SNAPSHOT.jar
- IDEA
添加-javaagent参数:-javaagent:D:\git\demo\AgentDemo\target\AgentDemo-1.0-SNAPSHOT.jar
其中,路径修改为自己的路径,另外,这里不能通过Program arguments的方式添加,这种方式会把agent参数添加到命令行的末尾,就不生效了:
agentmain(Attach模式)
Attach模式相对于Agent模式要麻烦一些,需要单独起一个应用(或者使用一个另外的线程),通过VirturalMachine.list()找到所有运行的VirtualMachineDescriptor,匹配到目标应用之后,再把javaagent.jar注入到目标应用里面去。
javaagent:
package org.example;
import java.lang.instrument.Instrumentation;
public class SimpleAgent {
public static void agentmain(final String agentArgs,
final Instrumentation inst) {
System.out.println("目标应用运行过程中,注入javaagent!");
}
}
目标应用main,while循环一直打印:
package org.example;
public class Main {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(1000);
System.out.println("Hello, world!");
}
}
}
Attach应用,只要运行一次,就会往目标应用注入一次:
package org.example;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class AttachApp {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
final List<VirtualMachineDescriptor> vmList = VirtualMachine.list();
for (final VirtualMachineDescriptor virtualMachineDescriptor : vmList) {
if (virtualMachineDescriptor.displayName().endsWith("org.example.Main")) {
final VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor);
vm.loadAgent("/path/to/SimpleAgent.jar");
vm.detach();
}
}
}
}
ClassFileTransformer
从上面两种javaagent的注入方式,可以看到JVM提供了两个时机点可以让我们对JVM进行调整。但这两个例子还太简单了,只是打印了一些字符串,而我们实际的工作中,我们是需要通过agent的形式,要么是收集一些信息,比如记录一些关键日志,或者提取一些信息,然后传递一些信息,比如灰度标记,或者是回放系统,录制流量,替换流量数据等。这些复杂的操作,在不需要目标应用开发人员修改代码的情况下,统一增强,就需要使用到ClassFileTransformer接口了。前面的agent里面,不管premain还是agentmain里面,都传入了一个Instrumentation的实例,这个实例里面就可以传入一个ClassFileTransformer的对象,我们就可以再ClassFileTransformer里面进行class的字节码增强。
javaagent:
package org.example;
import java.lang.instrument.Instrumentation;
public class SimpleAgent {
public static void agentmain(final String agentArgs,
final Instrumentation inst) {
System.out.println("目标应用运行过程中,注入javaagent!");
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException {
return null;
}
});
}
}
这个Transformer一旦加入之后,会对后面加载的所有的类进行转换,我们可以使用className来进行一些过滤。
但是有以下的局限性,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类:
- 新类和老类的父类必须相同;
- 新类和老类实现的接口数也要相同,并且是相同的接口;
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
- 新类和老类新增或删除的方法必须是private static/final修饰的;
- 可以修改方法体。
字节码增强工具
- ASM
- CgLib
- javassist
- byte buddy
ASM基本上是其他字节码增强技术的基础,绝大多数的工具都是在这个基础上建立的,但这个比较底层,需要了解具体的字节码规范,比较难用,和我们使用高级语言和汇编代码比较类似。另外三个工具,易用性和功能上是byte buddy > javassist > CgLib。所以,我们这里使用byte buddy来简单的演示一下,把所有的方法调用的耗时都打印一下。
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>AgentDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.22</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Main-Class>org.example.Main</Main-Class>
<Premain-Class>org.example.SimpleAgent</Premain-Class>
<Agent-Class>org.example.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
<Can-Retransform-Classes>false</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package org.example;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class SimpleAgent {
public static void premain(final String agentArgs,
final Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform((builder, typeDescription, classLoader, javaModule) ->
builder.method(ElementMatchers.any())
.intercept(MethodDelegation.to(TimingInterceptor.class))
).installOn(inst);
}
public static class TimingInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable) {
long start = System.currentTimeMillis();
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
System.out.println(method + " took " + (System.currentTimeMillis() - start));
}
}
}
}
java -javaagent:target/AgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar -jar target/AgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar
具体的各个字节码工具的用法不一一演示了,去找对应的帮助文档即可。基于这套原理,可以在不同的场合去实现很多的功能了,比如灰度、回放录制、重放等。因为我们引入了额外的maven jar包,这时再用maven-jar-plugin就不能正常工作了,因为引入的byte-buddy jar并不会打包到agent jar里面去,这时就需要maven-assembly-plugin来把依赖jar也打包到agent jar里面去了。
javaagent中依赖其他的jar
当javaagent中依赖其他的jar包时,我们在打包javaagent的jar时,需要把其他的jar打包进来,这时使用前面的maven-jar-plugin就不够了,要使用maven-assembly-plugin:
pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>org.example.SimpleAgent</Premain-Class>
<Agent-Class>org.example.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
<Can-Retransform-Classes>false</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
javaagent中依赖的jar和目标应用中的jar重复或者冲突
使用maven-assembly-plugin把javaagent依赖的jar打进去之后,有可能会和目标应用的jar产生冲突。这时,就需要我们把javaagent中依赖的jar的package进行一些重定位:
pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.SimpleAgent</Premain-Class>
<Agent-Class>org.example.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
<Can-Retransform-Classes>false</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>net.bytebuddy</pattern>
<shadedPattern>my.net.bytebuddy</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
参考链接
JVMTI Agent 工作原理及核心源码分析
maven-shade-plugin例子
javaagent包冲突解决方案