在类加载之后修改类的字节码
在类加载之后修改类的字节码
在类加载之后想要修改类的字节码必须要重新加载类,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类都成功被重转换,完成字节码插桩。