在类加载之后修改类的字节码

在类加载之后修改类的字节码

在类加载之后想要修改类的字节码必须要重新加载类,Instrumentation提供的retransformClasses方法可实现重转换类,即使用类转换器转换类后,再重新加载类。需要配合Attach Tools API使用,使用Attach Tools API在jvm进程运行的过程中,动态将一个Java Agent程序加载到jvm进程上,与该进程绑定执行。

我们将my-java-agent项目中的premain替换为agentmain,如代码清单7-14所示。

代码清单7-14 MyJavaAgent

public class MyJavaAgent {

    public static void agentmain(String agentOps, Instrumentation instrumentation) {
        System.out.println("agentmain function run...");
        BusinessClassFileTransformer transformer = new BusinessClassFileTransformer();
        // 给Instrumentation注册类转换器
        // 第二个:
        //    false: 注册一个不可重转换的转换器;
        //    true: 注册一个可重转换的转换器;
        instrumentation.addTransformer(transformer, true);

        // 获取所有已经加载的类
        Class<?>[] classs = instrumentation.getAllLoadedClasses();
        for (Class<?> cla : classs) {
            // 过滤掉java与sun包下的类
            if (cla.getName().startsWith("java")
                    || cla.getName().startsWith("sun")) {
                continue;
            }
            // 过滤掉与项目无关的一些类
            if (cla.getName().startsWith("com.intellij")
                    || cla.getName().startsWith("org.jetbrains")) {
                continue;
            }
            if (cla.getName().startsWith("[")) {
                continue;
            }
            // 把my-java-agent的包排除掉
            if (cla.getName().startsWith("com.wujiuye.agent")) {
                continue;
            }
            try {
                // 重转换类,重转换类不允许给类添加或移除字段
                instrumentation.retransformClasses(cla);
            } catch (UnmodifiableClassException e) {
                e.printStackTrace();
            }
        }
        // 完成后可将转换器移除
        instrumentation.removeTransformer(transformer);
    }

}

首先,我们在agentmain方法中为Instrumentation注册一个类转换器,类转换器还是使用前面我们编写的类转换器。在注册类转换器时,与前面premain方法使用的addTransformer方法不同,agentmain方法中调用addTransformer方法时第二个参数传true,即将类转换器注册为可重转换的类转换器。只有将该类转换器注册为可重转换的类转换器,重转换类时才会调用这个类转换器的transform方法。

在注册完类转换器之后,可调用Instrumentation的getAllLoadedClasses方法获取当前所有已经加载的类,然后遍历已经加载的类,过滤掉不需要插桩的类,最后调用Instrumentation的retransformClasses方法重转换类。调用retransformClasses方法之后,我们注册的类转换器BusinessClassFileTransformer的transform方法就会被调用。在agentmain方法的最后,可调用Instrumentation的removeTransformer方法将注册的类转换器移除。

我们还需要修改maven的pom依赖配置文件,为打包插件配置打包后的MANIFEST.MF文件,给MANIFEST.MF文件添加需要的参数。我们需要给MANIFEST.MF文件添加Agent-Class参数,即配置agentmain方法所在的类。代码如下:

    <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
            <!-- 把依赖的jar包一起打包 -->
            <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
            <archive>
                <!-- 注册agentmain的class -->
                <manifestEntries>
                    <Agent-Class>com.wujiuye.agent.MyJavaAgent</Agent-Class>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
        </configuration>
        <executions>
            <execution>
                <id>make-assembly</id>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
            </execution>
        </executions>
    </plugin>

除配置Agent-Class外,还需要配置Can-Retransform-Classes,即配置这个agent程序是否可以重转换类。配置生效的情况下,打包后jar包的MANIFEST.MF文件的内容如下:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: wjy
Build-Jdk: 1.8.0_144
Agent-Class: com.wujiuye.agent.MyJavaAgent
Can-Retransform-Classes: true

现在my-java-agent项目就已经改写完成了,使用agentmain方式无法像premain方式那样在启动命令指定agent程序的jar包路径就能被调用,需要借助Attach Tools API,将编写好的agent程序的jar包附着到目标jvm进程,agentmain才会被调用。因此,我们需要编写一个bootstart项目,用于查找本机上的jvm进程,将my-java-agent打包后的jar包附着到demo项目的jvm进程上。bootstart项目只有一个MyAgentBootstart类,如代码清单7-15所示。

代码清单7-15 MyAgentBootstart类

public class MyAgentBootstart {

    /**
     * 显示当前所有jvm进程
     *
     * @return
     */
    private static Map<Integer, String> showAllJavaProcess() {
        Map<Integer, String> pidMap = new HashMap<>();
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        int rows = 0;
        System.out.println("找到如下Java进程,请选择:");
        for (VirtualMachineDescriptor vmd : list) {
            pidMap.put(++rows, vmd.id());
            System.out.println("[" + rows + "] " + vmd.id() + "\t" + vmd.displayName().split(" ")[0]);
        }
        return pidMap;
    }

    /**
     * 读取选择
     *
     * @return
     * @throws IOException
     */
    private static String readString() throws IOException {
        char[] inputBuf = new char[1024];
        int leng = 0;
        while (true) {
            char ch = (char) System.in.read();
            if (ch == '\r') {
                break;
            }
            if (ch == '\n') {
                break;
            }
            inputBuf[leng++] = ch;
        }
        return new String(inputBuf, 0, leng);
    }

    /**
     * 将my-java-agent的jar包加载到目录进程
     *
     * @param pid jvm进程ID
     * @throws Exception
     */
    private static void attachPid(String pid) throws Exception {
        VirtualMachine vm;
        vm = VirtualMachine.attach(pid);
        System.out.println("attach pid:" + vm.id());
        try {
            vm.loadAgent("../my-java-agent-1.0-jar-with-dependencies.jar", null);
        } finally {
            vm.detach();
        }
    }

    public static void main(String[] args) throws Exception {
        Map<Integer, String> pidMap = showAllJavaProcess();
        Integer inputId = Integer.parseInt(readString());
        // 获取选择的进程ID
        String targetPid = pidMap.get(inputId);
        attachPid(targetPid);
    }

}

VirtualMachine API说明:

◆list:该方法可列出本机上所有运行中的jvm进程;

◆attach:选择一个jvm进程;

◆loadAgent:将agent程序的jar包加载到jvm进程,第一个参数是agent jar包的绝对路径,第二个参数是附加的参数。

agentmain方法被调用时,第一个参数的值就是loadAgent方法第二参数传递的附加参数。我们可使用这个参数实现一些配置,如配置只重转换某个类、配置只重转换某个包下的类等。

将demo程序改为如下:

public class DemoAppcliction {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main function runing...");
        UserService userService = new UserService();
        while (!Thread.interrupted()) {
            Map<String, Object> user = userService.queryUser("wujiuye", 25);
            System.out.println(user);
            Thread.sleep(10000);
        }
}

}

先启动demo程序,再启动bootstart程序,bootstart程序启动成功后会输出如图7.3所示的进程信息。

图7.3 bootstart程序启动成功后输出

demo程序的进程id是32000,在控制台输入1后回车即可将my-java-agent程序的jar包加载到pid为32000的jvm进程。附着成功后,demo程序输出如图7.4所示。

图7.4 demo程序输出

从图7.4可以看出,demo项目的UserService类和DemoApplication类都成功被重转换,完成字节码插桩。