需求
在不修改代码重新发布的情况下,查看一个线上正运行代码的方法的入参、返回值,或者增加某些日志的打印
解决方案
- arthas
- btrace
- 自定义classloader,发现需要动态加载的类定义变更时对类进行重新加载打破双亲委派
本文暂不讨论自定义classloader方式,工具的具体使用方法可以查看官方文档,那么这些工具的底层是如何实现的呢?没错他们底层都使用了JDK提供的Instrumentation接口。那么Instrumentation接口如何使用呢?
用法
以下内容为翻译的该接口的javadoc 该类提供测试Java编程语言代码所需的服务。Instrumentation是将字节码添加至方法中为收集工具所使用的数据。因为改变是纯添加动作,这些工具不会改变应用状态或行为。像这样良性的工具案例包含监控代理agents,profilers,coverage analyzers, and event loggers. 获取该接口实例的方法有两种:
- 当JVM通过显示的指定一个代理类的方式运行。在这种情形下,Instrumentation实例通过代理类的premain方法传入
- 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 复制代码