前言

总结java.lang.instrument.Instrumentation相关基础知识

课程目录

  • premain
  • agentmain

介绍

java中可以通过动态代理的方式来做一些AOP的事情,例如使用JDK ProxyCGLIBASMjavassist,在不修改源码的情况下在一个类的方法执行前后做一些事情。使用ASMjavassist包还可以直接修改编译后的class。

JDK自带的java.lang.instrument包其实也可以做到类似的事情,它也叫JavaAgent,或者java探针。它提供了两种方式来实现无侵入的instrument(监视?改造?增强?)你的java应用程序。可以结合ASMjavassist直接对字节码进行修改,让原有的功能更强大。

  • premain(JDK5)

顾名思义是在你的应用main方法之前就执行一些操作,可以通过java -javaagent:xxx.jar -jar xxx.jar的方式在JVM启动的时候,在main方法执行之前做一些事情,可以进行一些监控操作,或者直接修改原有代码。

  • agentmain(JDK6)

这个操作就更厉害了,可以在JVM启动之后attach到正在运行的JVM进程上,然后执行一些事情,监控原有代码,或者直接修改、替换字节码等等。

premain

假设有一个自己的应用程序,里面有一个方法,我们需要在方法执行之前添加一些代码,打印这个方法的参数信息。

创建业务应用

应用名称为instrument-demo-app,为了让这个简单的应用通过独立jar包可以运行,pom中添加maven-jar-plugin插件(在springboot应用中建议使用spring-boot-maven-plugin插件),使用maven的package命令直接打包成jar包instrument-demo-app-1.0.0.jar,并且我们使用maven插件来创建manifest文件一起打到jar包里面,相当于是在项目的src/main/resources/META-INF/MANIFEST.MF文件中添加一些属性。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>com.qigang.instrument.demo.app.TestApplication</mainClass>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>lib/</classpathPrefix>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>
public class TestApplication {
    public static void main(String[] args) throws InterruptedException {
        TestObject object = new TestObject();
        for(;;){
            TimeUnit.SECONDS.sleep(2);
            object.testMethod("zhangsan", "zhangsan@123.com",30);
        }
    }
}

public interface TestInterface {
    void testMethod(String name, String email, Integer age);
}

public class TestObject implements TestInterface {
    @Override
    public void testMethod(String name, String email, Integer age) {
        System.out.println("testMethod begin");
        System.out.println(String.format("name:%s, email:%s, age:%s", name,email,age));
        System.out.println("testMethod end");
    }
}

创建premain探针应用

应用名称为instrument-demo-premain,结合javassist来修改instrument-demo-app-1.0.0.jar中原有的方法字节码,需要添加javassist依赖,以及打成独立jar包的maven插件,注意manifest的配置,可以参考java.lang.instrument中的说明。最终打成的探针jar包为instrument-demo-premain-1.0.0.jar。

<dependencies>
    <!--使用javassit修改字节码-->
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.23.1-GA</version>
    </dependency>
    <!--工具包-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.5</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>lib/</classpathPrefix>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>com.qigang.instrument.demo.premain.MyPreMainApplication</Premain-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <id>copy</id>
                    <phase>package</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>${project.build.directory}/lib</outputDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

java代码包括3个类:1. premain方法入口类,2. ClassFileTransformer类负责修改原有方法字节码注入日志代码行,3. 单独的日志打印类

import java.lang.instrument.Instrumentation;
public class MyPreMainApplication extends Object {
    public static void premain(String agentArgs, Instrumentation inst) {
        //这里的addtransformer添加的ClassFileTransformer是针对所有的类的,每个类被加载的时候,MyClassFileTransformer.transform方法都会被执行一次
        //所以在MyClassFileTransformer.transform方法中要做好过滤,只处理需要修改的类
        inst.addTransformer(new MyClassFileTransformer());
    }
}

import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import org.apache.commons.lang3.StringUtils;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.Set;

public class MyClassFileTransformer implements ClassFileTransformer {

    private static Set<String> interFaceList = new HashSet<>();

    static {
        interFaceList.add("com.qigang.instrument.demo.app.TestInterface");
    }

    /**
     * transform方法会处理所有加载的类
     * @param loader
     * @param className
     * @param classBeingRedefined
     * @param protectionDomain
     * @param classfileBuffer
     * @return
     * @throws IllegalClassFormatException
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (StringUtils.isBlank(className)) {
                return null;
            }
            String currentClassName = className.replaceAll("/", ".");
            CtClass currentClass = ClassPool.getDefault().get(currentClassName);
            CtClass[] interfaces = currentClass.getInterfaces();
            if (!needProcess(interfaces)) {
                return null;
            }
            //引入需要使用的class对应的包
            ClassPool.getDefault().importPackage("com.qigang.instrument.demo.premain");
            CtBehavior[] methods = currentClass.getMethods();
            for (CtBehavior method : methods) {
                String methodName = method.getName();
                if ("testMethod".equals(methodName)) {
                    CtClass[] paramTypes = method.getParameterTypes();
                    for(int i=0;i<paramTypes.length;i++){
                        CtClass paramType = paramTypes[i];
                        String typeName = paramType.getName();
                        if ((String.class.getName().replaceAll("/", ".")).equals(typeName)) {
                            //如果方法参数类型为字符串,则打印该参数值,下标从1开始
                            method.insertAt(0, " MyLogger.log($"+(i+1)+");");
                        }
                    }
                }
            }
            return currentClass.toBytecode();

        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    private boolean needProcess(CtClass[] interfaces) {
        for (int i = 0; i < interfaces.length; i++) {
            if (interFaceList.contains(interfaces[i].getName())) {
                return true;
            }
        }
        return false;
    }
}

public class MyLogger {
    public static void log(String message) {
        System.out.println("this is insert log :" + message);
    }
}

测试

1.将instrument-demo-app打成jar包instrument-demo-app-1.0.0.jar,将探针应用instrument-demo-premain-1.0.0.jar打成jar包instrument-demo-premain-1.0.0.jar(依赖包在target/lib/目录下),为了测试方便,把2个jar包以及target/lib/目录全部copy到一起

2.启动应用instrument-demo-app-1.0.0.jar,通过-javaagent参数添加探针,可以看到日志打印代码已经被注入到方法中并执行

java -javaagent:instrument-demo-premain-1.0.0.jar -jar instrument-demo-app-1.0.0.jar
----------------------------------------------
this is insert log :zhangsan@123.com
this is insert log :zhangsan
testMethod begin
name:zhangsan, email:zhangsan@123.com, age:30
testMethod end
----------------------------------------------

agentmain

还是使用上面的例子,instrument-demo-app应用的代码逻辑保持不变,我们现在先让instrument-demo-app-1.0.0.jar应用先跑起来,然后再将我们的探针应用jar包attach到跑起来的应用JVM进程上,在程序执行的过程中修改字节码。给奔跑中的汽车换轮子,很厉害的样子。

创建agentmain探针应用

应用名称为instrument-demo-agentmain,pom文件中的依赖以及maven插件与premain探针应用基本一样,除去manifest中的Premain-Class属性需要修改成Agent-Class

<dependencies>
        <!--使用javassit修改字节码-->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.23.1-GA</version>
        </dependency>
        <!--工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.qigang.instrument.demo.agentmain.MyAgentMainApplication</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

java代码也还是包括3个类:1. agentmain方法入口类,这个同premain探针入口方法有点区别,2. ClassFileTransformer子类不变,3. 单独的日志打印类也不变

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

public class MyAgentMainApplication {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain starting ****************************");

        inst.addTransformer(new MyClassFileTransformer(),true);

        try {
            //这一步很关键,探针被执行的时候需要主动转换需要改造的class
            inst.retransformClasses(Class.forName("com.qigang.instrument.demo.app.TestObject"));
        } catch (UnmodifiableClassException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}



import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import org.apache.commons.lang3.StringUtils;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.Set;

public class MyClassFileTransformer implements ClassFileTransformer {

    private static Set<String> interFaceList = new HashSet<>();

    static {
        interFaceList.add("com.qigang.instrument.demo.app.TestInterface");
    }

    /**
     * transform方法会处理所有加载的类
     * @param loader
     * @param className
     * @param classBeingRedefined
     * @param protectionDomain
     * @param classfileBuffer
     * @return
     * @throws IllegalClassFormatException
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println("transformer starting, className: "+className);

        try {
            if (StringUtils.isBlank(className)) {
                return null;
            }
            String currentClassName = className.replaceAll("/", ".");
            CtClass currentClass = ClassPool.getDefault().get(currentClassName);
            CtClass[] interfaces = currentClass.getInterfaces();
            if (!needProcess(interfaces)) {
                return null;
            }
            //引入需要使用的class对应的包,这里引入的命名空间要正确
            ClassPool.getDefault().importPackage("com.qigang.instrument.demo.agentmain");
            CtBehavior[] methods = currentClass.getMethods();
            for (CtBehavior method : methods) {
                String methodName = method.getName();
                if ("testMethod".equals(methodName)) {
                    CtClass[] paramTypes = method.getParameterTypes();
                    for(int i=0;i<paramTypes.length;i++){
                        CtClass paramType = paramTypes[i];
                        String typeName = paramType.getName();
                        if ((String.class.getName().replaceAll("/", ".")).equals(typeName)) {
                            //如果方法参数类型为字符串,则打印该参数值,下标从1开始
                            method.insertAt(0, " MyLogger.log($"+(i+1)+");");
                        }
                    }
                }
            }
            return currentClass.toBytecode();

        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    private boolean needProcess(CtClass[] interfaces) {
        for (int i = 0; i < interfaces.length; i++) {
            if (interFaceList.contains(interfaces[i].getName())) {
                return true;
            }
        }
        return false;
    }
}


public class MyLogger {
    public static void log(String message) {
        System.out.println("this is insert log :" + message);
    }
}

attach探针应用到运行中的JVM应用进程上

想要将我们做好的agentmain探针应用生效,需要attach到运行中的JVM进程上,这需要用到JDK自带的tools.jar这个工具包,为了更好的隔离应用,我们创建一个单独的应用专门用来做attach这个事情,这个应用名称为instrument-demo-agentmain-attach,它的代码比较简单,不需要任何配置文件和pom依赖:

import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;

public class MyAttachApplication {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor descriptor : list) {
            if (descriptor.displayName().endsWith("instrument-demo-app-1.0.0.jar")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
                virtualMachine.loadAgent("E:\\agent\\instrument-demo-agentmain-1.0.0.jar", "arg1");
                virtualMachine.detach();
            }
        }
    }
}

tools.jar默认情况下不在classpath中,这个jar包在${JAVA_HOME}/lib目录下面,将tools.jar添加到IDEA的library中之后就可以正常运行(Project Structure–>Libraries–>Add)。

测试

1.将instrument-demo-app打成jar包instrument-demo-app-1.0.0.jar

2.将instrument-demo-agentmain打成jar包instrument-demo-agentmain-1.0.0.jar,依赖jar包都在target/lib目录下

3.为了测试方便,将2个jar包以及依赖包目录target/lib都copy到同一个目录(这里测试目录为E:\agent)下面

4.启动instrument-demo-app-1.0.0.jar,然后再执行instrument-demo-agentmain-attach进行探针包的attach动作(直接在idea中运行),会发现输出的结果发生了变化,代表探针修改字节码操作生效,参数日志信息也打印出来了

java -jar instrument-demo-app-1.0.0.jar

----------------------------------------------------------------------------
testMethod begin--------------------------------Mon Aug 26 17:01:08 CST 2019
name:zhangsan, email:zhangsan@123.com, age:30
testMethod end--------------------------------
testMethod begin--------------------------------Mon Aug 26 17:01:10 CST 2019
name:zhangsan, email:zhangsan@123.com, age:30
testMethod end--------------------------------
agentmain starting ****************************
transformer starting, className: com/qigang/instrument/demo/app/TestObject
transformer starting, className: com/qigang/instrument/demo/agentmain/MyLogger
this is insert log :zhangsan@123.com
this is insert log :zhangsan
testMethod begin--------------------------------Mon Aug 26 17:01:12 CST 2019
name:zhangsan, email:zhangsan@123.com, age:30
testMethod end--------------------------------
this is insert log :zhangsan@123.com
this is insert log :zhangsan
testMethod begin--------------------------------Mon Aug 26 17:01:14 CST 2019
name:zhangsan, email:zhangsan@123.com, age:30
testMethod end--------------------------------
----------------------------------------------------------------------------

agentmain探针和attach应用打包在一起

上面的操作是agentmain探针应用打成jar包,attach应用测试时直接在idea中运行。其实可以将两者打成一个jar包,attach的JVM进程名称可以作为参数来指定,探针jar包的路径可以动态获取。具体可以查看instrument-demo-agentmain源码。

其它

其实instrument还有其它用途:

  • 修改classpath

主要依赖appendToBootstrapClassLoaderSearchappendToSystemClassLoaderSearch这两个方法,从名称可以判断出意思。

  • 针对本地代码的instrumentation

较少使用,不介绍了。

参考

java.lang.instrument

JAVA instrument简单使用

*****动态代理方案性能对比(梁飞博客)

Instrumentation 新功能

使用maven-jar-plugin定制MANIFEST.MF文件