需求
在不修改代码重新发布的情况下,查看一个线上正运行代码的方法的入参、返回值,或者增加某些日志的打印
解决方案
arthas
btrace
自定义classloader,发现需要动态加载的类定义变更时对类进行重新加载打破双亲委派
本文暂不讨论自定义classloader方式,工具的具体使用方法可以查看官方文档,那么这些工具的底层是如何实现的呢?没错他们底层都使用了JDK提供的Instrumentation接口。那么Instrumentation接口如何使用呢?
用法
以下内容为翻译的该接口的javadoc 该类提供测试Java编程语言代码所需的服务。Instrumentation是将字节码添加至方法中为收集工具所使用的数据。因为改变是纯添加动作,这些工具不会改变应用状态或行为。像这样良性的工具案例包含监控代理agents,profilers,coverage analyzers, and event loggers. 获取该接口实例的方法有两种:
当JVM通过显示的指定一个代理类的方式运行。在这种情形下,Instrumentation实例通过代理类的premain方法传入
JVM提供一个机制在JVM运行之后启动代理。在这种情形下,Instrumentation实例通过代理代码的agentmain方法传入
显示指定代理类方式获取
案例代码取自arthas pom配置为生成的manifest文件中指定premain代码提供类以及代理类提供类
- <plugin>
- <groupId>org.apache.maven.pluginsgroupId>
- <artifactId>maven-assembly-pluginartifactId>
- <executions>
- <execution>
- <goals>
- <goal>singlegoal>
- goals>
- <phase>packagephase>
- <configuration>
- <descriptorRefs>
- <descriptorRef>jar-with-dependenciesdescriptorRef>
- descriptorRefs>
- <archive>
- <manifestEntries>
- <Premain-Class>com.taobao.arthas.agent.AgentBootstrapPremain-Class>
- <Agent-Class>com.taobao.arthas.agent.AgentBootstrapAgent-Class>
- <Can-Redefine-Classes>trueCan-Redefine-Classes>
- <Can-Retransform-Classes>trueCan-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