Java Instrumentation初体验

403 阅读3分钟

介绍

  • JVM Tool Interface(JVMTI): JVM提供的一套API,用来供开发者通过代理的方式来监控和控制应用程序。

注意,JVMTI是通过代理的方式来实现,因此需要自己开发agent程序,另外,这里的agent程序是C/C++编写的程序编译之后生成的动态链接库,因此,为了方便java开发者开发agent程序,在Java SE5中新加入了Instrumentation机制,该机制底层仍是基于JVMTI,只不过以Java API的方式暴露出来,减小了开发的难度。

本文通过Instrumentation开发一个简单的agent,通过agent更改类的字节码的方式动态修改方法体,希望通过这个例子能让大家对Instrumentation有个直观的认识。

实践

1.定义简单类和主程序类

TransClass:

public class TransClass {
    public int getNumber() {
        return 10;
    }
}

TestMain:

public class TestMain {

    public static void main(String[] args) {
        System.out.println(new TransClass().getNumber());
    }
}

打成jar包agent-demo-main-1.0-SNAPSHOT.jar后运行:

可以看到返回结果为10.

2.编写agent

Java agent是以jar包形式依附在应用程序上,一般是通过java -javaagent:TestAgent.jar Main命令将agent挂载到主程序Main上。

2.1 agent入口类

package com.funstar;

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;


public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println("执行premain");
        inst.addTransformer(new Transformer());
    }

}

premain函数是Java agent入口函数,参数String agentArgs通过java命令传入,Instrumentation inst是Instrumentation实例,由JVM自动传入,其接口定义如下:

public interface Instrumentation {
    /**
     * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
     * Transformer可以直接对类的字节码byte[]进行修改
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
     * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    /**
     * 获取一个对象的大小
     */
    long getObjectSize(Object objectToSize);
    
    /**
     * 将一个jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    
    /**
     * 获取当前被JVM加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

通过addTransformer添加ClassFileTransformer实例后,后续所有加载的类都会执行ClassFileTransformer.transform方法逻辑,因此可以在这个方法里对需要修改的类进行过滤拦截。

2.2 ClassFileTransformer实现类

public class Transformer implements ClassFileTransformer {

    public static final String classNumberReturns2 = "MyTransClass.class";

    public static byte[] getBytesFromFile(String fileName) {
        try {
            InputStream is = Transformer.class.getClassLoader().getResourceAsStream(fileName);
            ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
            byte[] buff = new byte[100];
            int rc = 0;
            while ((rc = is.read(buff, 0, 100)) > 0) {
                swapStream.write(buff, 0, rc);
            }
            byte[] byteArray = swapStream.toByteArray();
            if (null == byteArray || byteArray.length == 0){
                return null;
            }
            return byteArray;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            System.out.println(e.getMessage());
            return null;
        }
    }

    @Override
    public byte[] transform(ClassLoader l, String className, Class<?> c,
                            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        if (!className.contains("TransClass")) {
            return null;
        }

        return getBytesFromFile(classNumberReturns2);
    }
}

Transformer的逻辑是过滤类名为TransClass类,并通过MyTransClass.class文件内容替换这个类的定义。

2.3 修改TransClass逻辑,生成字节码文件

public class TransClass {
    public TransClass() {
    }

    public int getNumber() {
        return 1000;
    }
}

编译这个文件后,重命名为MyTransClass.class,并放到classpath下,agent的目录结构如下:

2.4 生成agent jar包

打包生成的agent jar包的META-INF/MAINIFEST.MF文件中需要加入Premain-Class来指定agent入口类(有premain函数的类),可以通过maven-jar-plugin插件实现:

<plugin>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.0.2</version>
        <configuration>
            <archive>
                <manifest>
                    <addClasspath>true</addClasspath>
                </manifest>
                <manifestEntries>
                    <Premain-Class>com.funstar.Premain</Premain-Class>
                </manifestEntries>

             </archive>
        </configuration>
</plugin>

执行mvn package后包名为agent-demo-premain-1.0-SNAPSHOT.jar。 将agent挂载到原主程序重新执行

可以看到TransClass的方法体已经被修改,返回结果是1000.

参考资料

基于Java Instrument的Agent实现