该笔记百分之八十借鉴自美团技术团队文章:Java动态追踪技术探究
引子(怎么样修改已经编译的代码)
在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:
“Debug一下吧。”
“线上机器,没开Debug端口。”
“看日志,看看请求值和返回值分别是什么?”
“那段代码没打印日志。”
“改代码,加日志,重新发布一次。”
“怀疑是线程池的问题,重启会破坏现场。”
长达几十秒的沉默之后:“据说,排查问题的最高境界,就是只通过Review代码来发现问题。”
比几十秒长几十倍的沉默之后:“我轮询了那段代码一十七遍之后,终于得出一个结论。”
“结论是?”
“我还没到达只通过Review代码就能发现问题的至高境界。”
JSP的特别编译
JSP文件修改过后,之所以能及时生效,是因为Web容器(Tomcat)会检查请求的JSP文件是否被更改过。如果发生过更改,那么就将JSP文件重新解析翻译成一个新的Sevlet类,并加载到JVM中。之后的请求,都会由这个新的Servet来处理。这里有个问题,根据Java的类加载机制,在同一个ClassLoader中,类是不允许重复的。为了绕开这个限制,Web容器每次都会创建一个新的ClassLoader实例,来加载新编译的Servlet类。之后的请求都会由这个新的Servlet来处理,这样就实现了新旧JSP的切换。
怎么直接改变java的对象
首先Java的对象行为(方法、函数)是存储在方法区的。
“方法区中的数据从哪来?”
“方法区中的数据是类加载时从class文件中提取出来的。”
“class文件从哪来?”
“从Java或者其他符合JVM规范的源代码中编译而来。”
“源代码从哪来?”
“废话,当然是手写!”
“倒着推,手写没问题,编译没问题,至于加载……有没有办法加载一个已经加载过的类呢?
如果有的话,我们就能修改字节码中目标方法所在的区域,然后重新加载这个类,这样方法区中的对象行为(方法)就被改变了,而且不改变对象的属性,也不影响已经存在对象的状态,那么就可以搞定这个问题了。可是,这岂不是违背了JVM的类加载原理?
有意思的类Instrumentation
java.lang.instrument.Instrumentation
- 重新定义class文件
/*
使用所提供的类文件重新定义所提供的一组类。
此方法用于在不引用现有类文件字节的情况下替换类的定义,就像从源代码重新编译以修复并继续调试时可能会做的那样。
在要转换现有类文件字节的地方(例如在字节码插入中),应该使用retransformClasses。
此方法对集合进行操作,以便允许同时对多个类进行相互依赖的更改(对类a的重新定义可能需要对类B的重新定义)。
如果重新定义的方法具有活动堆栈帧,那么这些活动帧将继续运行原始方法的字节码。重新定义的方法将用于新的调用。
该方法不会引起任何初始化,除非在常规JVM语义下会发生初始化。
换句话说,重新定义一个类不会导致它的初始化程序运行。静态变量的值将保持为cal之前的值
*/
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException,UnmodifiableClassException;
- 修改class文件
/*
重新传输提供的类集。
此函数便于插入已加载的类。当最初加载类或重新定义类时,可以使用ClassFileTransformer转换初始类文件字节。
此函数会重新运行转换过程(无论以前是否发生过转换)。
这种重新转换遵循以下步骤:
从初始类文件字节开始
对于每个添加了canRetransform false的转换器,在最后一次类加载或重定义期间转换返回的字节将被重新用作转换的输出;
请注意,这相当于重新应用之前的转换,没有改变;
除了没有调用转换
对于每个添加了canRetransform true的转换器,在这些转换器中调用转换方法
转换后的类文件字节将作为类的新定义安装
变换的顺序在变换
*/
void retransformClasses(Class<?>... classes)
throws UnmodifiableClassException;
看完说明
两个都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。
redefineClasses在运行时直接替换类是不安全的操作。
比如新的class文件引用了一个不存在的类,或者把某个类的一个field给删除了等等,这些情况都会引发异常。所以如文档中所言,instrument存在诸多的限制:
The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.
机翻:
重新定义可能会更改方法体、常量池和属性。重新定义不得添加、删除或重命名字段或方法,不得更改方法的签名或更改继承。这些限制可能会在未来版本中取消。在应用转换之前,不会检查、验证和安装类文件字节,如果生成的字节出错,此方法将引发异常。
只对方法进行简单的修改行为对于打印日志已经够了,reTransform的功能已经可以满足了
怎么得到class文件
最简单的方法就是把代码修改后再编译得到一个class文件,然后调用redefineClass替换,
如果拿不到或者拿到文件很困难,这该怎么办
有没有一种可能我们直接修改class文件,而且已经有程序员提供直接编辑字节码文件的框架了,能够提供接口可以让我们方便地操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。
ASM是其中最著名的框架,cglib、Spring等框架中对于字节码的操作就建立在ASM之上。
ASM,Javaagent -》cglib-》springAOP
我们都知道,Spring的AOP是基于动态代理实现的,Spring会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。那么,Spring是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring在运行时会根据需要动态地创造出一个类,这里创造的过程并非通过字符串写Java文件,然后编译成class文件,然后加载。
Spring会直接“创造”一个class文件,然后加载,创造class文件的工具,就是ASM了
。
BTrace
怎么实现上面的操作
A safe, dynamic tracing tool for the Java platform.
机翻:
适用于 Java 平台的安全动态跟踪工具
BTrace是基于Java语言的一个安全的、可提供动态追踪服务的工具。BTrace基于ASM、Java Attach Api、Instruments开发,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)达到我们想要的效果,而不必深陷于ASM对字节码的操作中不可自拔。
源文章里面有个例子自己点进去看
BTrace主要有下面几个模块:
- BTrace脚本:利用BTrace定义的注解,我们可以很方便地根据需要进行脚本的开发。
- Compiler:将BTrace脚本编译成BTrace class文件。
- Client:将class文件发送到Agent。
- Agent:基于Java的Attach Api,Agent可以动态附着到一个运行的JVM上,然后开启一个BTrace Server,接收client发过来的BTrace脚本;解析脚本,然后根据脚本中的规则找到要修改的类;修改字节码后,调用Java Instrument的reTransform接口,完成对对象行为的修改并使之生效。
BTrace的架构
BTrace最终借Instruments实现class的替换。如上文所说,出于安全考虑,Instruments在使用上存在诸多的限制,BTrace也不例外。BTrace对JVM来说是“只读的”,因此BTrace脚本的限制如下:
- 不允许创建对象
- 不允许创建数组
- 不允许抛异常
- 不允许catch异常
- 不允许随意调用其他对象或者类的方法,只允许调用com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)
- 不允许改变类的属性
- 不允许有成员变量和方法,只允许存在static public void方法
- 不允许有内部类、嵌套类
- 不允许有同步方法和同步块
- 不允许有循环
- 不允许随意继承其他类(当然,java.lang.Object除外)
- 不允许实现接口
- 不允许使用assert
- 不允许使用Class对象
BTrace要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。
Arthas
BTrace脚本又学习成本,而且复杂,现在很多公司已经在使用阿里巴巴的java诊断工具-Arthas Arthas提供简单的命令行操作,功能强大。究其背后的技术原理,和本文中提到的大致无二。Arthas的文档很全面,想详细了解的话可以戳这里。