Java动态编程之Instrumentation

从jdk1.5开始提供Instrumentation接口

需求

在不修改代码重新发布的情况下,查看一个线上正运行代码的方法的入参、返回值,或者增加某些日志的打印

解决方案

  • arthas
  • btrace
  • 自定义classloader,发现需要动态加载的类定义变更时对类进行重新加载打破双亲委派

本文暂不讨论自定义classloader方式,工具的具体使用方法可以查看官方文档,那么这些工具的底层是如何实现的呢?没错他们底层都使用了JDK提供的Instrumentation接口。那么Instrumentation接口如何使用呢?

用法

以下内容为翻译的该接口的javadoc
该类提供测试Java编程语言代码所需的服务。Instrumentation是将字节码添加至方法中为收集工具所使用的数据。因为改变是纯添加动作,这些工具不会改变应用状态或行为。像这样良性的工具案例包含监控代理agents,profilers,coverage analyzers, and event loggers.
获取该接口实例的方法有两种:

  1. 当JVM通过显示的指定一个代理类的方式运行。在这种情形下,Instrumentation实例通过代理类的premain方法传入
  2. JVM提供一个机制在JVM运行之后启动代理。在这种情形下,Instrumentation实例通过代理代码的agentmain方法传入

翻译结束23333

显示指定代理类方式获取

案例代码取自arthas
pom配置为生成的manifest文件中指定premain代码提供类以及代理类提供类

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>single</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestEntries>
                        <Premain-Class>com.taobao.arthas.agent.AgentBootstrap</Premain-Class>
                        <Agent-Class>com.taobao.arthas.agent.AgentBootstrap</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        <Specification-Title>${project.name}</Specification-Title>
                        <Specification-Version>${project.version}</Specification-Version>
                        <Implementation-Title>${project.name}</Implementation-Title>
                        <Implementation-Version>${project.version}</Implementation-Version>
                    </manifestEntries>
                </archive>
            </configuration>
        </execution>
    </executions>
</plugin>

代理类、premain方法提供类编写接收Instrumentation实例的方法

// 通过-agent方法显示指定方式启动JVM时会回调该方法传入Instrumentation实例
public static void premain(String args, Instrumentation inst) {
    main(args, inst);
}

将premain提供类打成jar包。启动其他jvm时将该该jar作为agent参数出入即可,启动命令如下:

java -javaagent "${HOME}/.arthas/lib/3.1.0/arthas/arthas-agent.jar" ...

JVM启动后再启动代理方式获取

同上中方式相同,maven的pom配置为生成的manifest文件中指定agent代码提供类
代理类方法提供类编写接收Instrumentation实例的方法

// JVM启动后,通过VM.attach方式附加至目标JVM时回调该方法传入Instrumentation
public static void agentmain(String args, Instrumentation inst) {
    main(args, inst);
}

将代理类附加到目标JVM

private void attachAgent(Configure configure) throws Exception {
    // 根据PID获取目标JVM描述实例
    VirtualMachineDescriptor virtualMachineDescriptor = null;
    for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
        String pid = descriptor.id();
        if (pid.equals(Integer.toString(configure.getJavaPid()))) {
            virtualMachineDescriptor = descriptor;
        }
    }
    VirtualMachine virtualMachine = null;
    try {
        // 通过pid方式直接依附至目标JVM
        if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
            virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
        } else {
            // 通过JVM描述符方式依附
            virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
        }
    ...
        // 为依附上的目标JVM加载代理jar
        virtualMachine.loadAgent(arthasAgentPath,
                configure.getArthasCore() + ";" + configure.toString());
    ...
}

通过Instrumentation转换类字节码

实现ClassFileTransformer接口的transform方法

@Override
public byte[] transform(final ClassLoader inClassLoader, String className, Class<?> classBeingRedefined,            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        ...
		// inClassLoader:将要被转换类所在的类加载器,如果是bootstrap loader则该参数为null
		// className:将要被转换类的全限定名称
		// classBeingRedefined:将要被转换类的class
		// protectionDomain:将要被转换类的保护域
		// classfileBuffer:将要被转换类的字节码
		// 修改增强后的字节码
        return enhanceClassByteArray;
        ...
}

将ClassFileTransformer实现添加至Instrumentation,并执行转换

// Enhancer为arthas中提供的ClassFileTransformer实现类
final Enhancer enhancer = new Enhancer(adviceId, isTracing, skipJDKTrace, enhanceClassSet, methodNameMatcher, affect);
// 将转换类添加至Instrumentation
inst.addTransformer(enhancer, true);
...
// 执行转换
inst.retransformClasses(clazz);

总结

  • instrumentation.addTransformer的第二个参数canRetransform如果不指定默认为false,如果为true,则在显示调用retransformClasses时会立即回调。
  • jdk api文档中指明在转换类定义之前已经实例化的对象不会受影响,这种说法指的应该是因为对象不能新增、删除、更改字段方法等,所以类型的实例不会受到重定义的影响;但是对于方法的修改会生效,官方也有说明:已经入栈的方法不会受影响,但是对于下一次方法调用则会使用新的定义
  • ClassFileTransformer实例中要对类名称进行匹配,因为其他类加载时也可能会走到该转换类中导致出现异常,例如:
###### start transform
loader:sun.misc.Launcher$AppClassLoader@18b4aac2, className:null, classBeingRedefined:null
###### end transform
发布了81 篇原创文章 · 获赞 85 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u010597819/article/details/103657830