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包冲突解决方案

posted @ 2022-09-19 15:31  yangwen0228  阅读(982)  评论(2编辑  收藏  举报