Java动态追踪技术
- 需求翻译官的日常工作不是在的改bug,发布代码,就是在加日志查找bug的路上。查BUG的过程是痛苦的,我们总是在不停的看代码,修改代码,添加日志,从而帮助我们发现问题,这种形式是比较繁琐的,不断的在发布项目,开发效率也不高,而且线上的问题排查不可能让你经常添加调试代码,能不能找到一种和JSP一样快捷的方式呢。
JSP模式
-
对应大多数程序员来说,早期的时候都接触过JSP(java server pages)。虽然之后在前后端分离的主流思想下主键淘汰不用了,但是JSP还是有一些比较值得我们去思考的点,例如我们在使用JSP写页面效果的时候,修改代码只需要刷新页面,而不需重启服务,就可以看到页面展示效果。并不需要重启JVM。
-
按照我们的常识,java程序一般都需要启动时候加载类文件,如果像JSP这样修改完代码,不用重启就生效,那么我们就不用这么痛苦的查bug,直接加代码又不用发,加日志看下就解决了。其实是JSP的运行机制和java不一样,当我们打开浏览器,请求一个JSP文件有如下流程:
-
JSP文件修改后,之所以能及时生效,是因为tomcate会检查请求的JSP文件是否被更改过。如果发送过更改,呢么就会将JSP文件重新解析,翻译成一个新的java,从而产生新的servlet类,加载到jvm中。
-
但是此处有一个问题,根据java类加载机制,同一个classLoader中,类是不允许重复的。为绕开这个限制,Tomcate每次会创建一个新的ClassLoader实例,来加载新编译的servlet类。之后的请求都会有这个新的Servlet来处理,这样就实现了新旧JSP的切换。
-
HTTP服务是无状态的,所以JSP的场景基本上是一次性消费,请求后给当前解析后的jsp内容,这种通过ClassLoader来替换class的方法在JSP上可行,因为有一个翻译过程,但是在Spring应用中,对象大多是单例,存在内存中,就算创建新ClassLoader也要全部在处理一次,不太现实,所有这种方式在Spring项目中不可行。
-
BTrace 插桩
java 对象行为
- java对象使用两种东西来描述事物: 方法,属性
- java对象的属性跟着对象走,每个对象存储一份
- java对象的方法,函数存储在方法区
- 如上图:
- 方法区中数据是类加载时候从class文件中取出
- class文件是从java或者其他符号jvm规范的源码中编译来的
- 源码我们我们可以自己控制
- 通过上面几个步骤我们能否找到办法去修改需要加载的类,从而达到我们的目的,并且需要满足几个条件:
- 需要修改字节码中目标方法所在区域,然后重新加载这个类
- 只修改调用方法,不修改对象的属性,也不印象已经存在的对象状态
- 不违背jvm类加载原理,也就是这个类还是这个类,还是同一个ClassLoader
- 还真有:java.lang.instrument.Instrumentation
Instrumentation
- 我们来查一下Java API中对这个类的描述信息:
该类提供了用于设计Java编程语言代码所需的服务。 仪器是向方法添加字节码,用于收集工具要使用的数据。 由于这些更改纯粹是加法的,因此这些工具不会修改应用程序的状态或行为。 这种良性工具的示例包括监视代理,剖析器,覆盖分析器和事件记录器。
- 文档描述中有两个我们能用得到的方法:redefineClasses和retransformClasses。一个重新定义class,一个修改class。这两个差不多功能,
// 使用提供的类文件重新定义提供的一组类。
void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException,UnmodifiableClassException
//重新转换提供的一组类。
void retransformClasses(类<?>... classes)throws UnmodifiableClassException
- 两个方法都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换已经存在的class,retransformClasses是在已经存在的字节码文件上修改后在替换。
- 既然JDK提供了这种API,那么我们可以在编译得到class文件后,在通过redefineClass替换,就能加日志,修改class文件,从而达到上文中不重启修改的目的。
直接操作字节码
- 我们通过JDK的api来修改了本要加载的一个Class字节码文件,字节码文件也是程序语言,只不过人类不好理解,可读性远没有java代码高。
- 一般人都不会去直接修改字节码文件,但是,有一部分杰出的程序员,创造出来可以直接编辑字节码的框架,提供接口可以让我们方便的去操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。其中最著名的就是ASM,现在我们接触的cglib,Spring等框架中对字节码的操作就是基于ASM上的,
实现方式
-
截止目前都是针对开始遇到的问题的理论层面的可行性研究,那么我们怎么实现,而且修改线上的字节码文件简直就是在作死,而且实施起来会有意想不到的困难:
- 寻找工程中的这个字节码
- 修改这个字节码,然后reTransform这个字节码
- 我们无法预知某个程序出错需要去修改字节码,也不可能每个工程都开发一段专门的程序去修改字节码,并且重新加载改代码,这样成本太高
- 即使我们能解决上面的问题,我们也不一定会用ASM,需要更通用的办法
- JVM不在本地,在远程,我们也是需要解决的问题
- 修改线上JVM中字节码文件简直在作死,这存在巨大的安全性问题
-
幸运的是,已经有人吧这么都搞定了,因为有一个开源工具BTrace的存在,描述非常精炼:
A safe, dynamic tracing tool for the Java platform
-
BTrace 是基于Java预约的一个安全,可提供动态追踪服务的工具。BTrace基于ASM,Java Attach API, Instruments开发,为用户提供很多注解,依靠这些注解,可以编写BTrace脚本(简单的Java脚本)达到我们的目的,而不必对ASM有深刻理解。
-
我们看下Attach 的api
-
看BTrace官网的一个简单例子:拦截所有java.io包中所有类以read开头的方法,打印类名,方法名,参数名。当程序IO负载比较高的时候,可以从输出的信息看到是哪些类引起的,如此的方便:
package com.sun.btrace.samples;import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;/*** This sample demonstrates regular expression* probe matching and getting input arguments* as an array - so that any overload variant* can be traced in "one place". This example* traces any "readXX" method on any class in* java.io package. Probed class, method and arg* array is printed in the action.*/
@BTrace public class ArgArray {@OnMethod(clazz="/java\\.io\\..*/",method="/read.*/")public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {println(pcn);println(pmn);printArray(args);}
}
先理解在应用
-
既然BTrace能解决以上的问题,那么他是怎么做到的,他的架构是怎么样的,我们可以从管网的信息中找到答案:
-
BTrace主要有一下几个模块
- BTrace脚本:利用BTrace定义的注解,我们可以很方便的更具需要进行脚本开发
- Compiler(编译):将BTrace脚本编译成BTrace class文件
- Client:将Class文件发送到Agent
- Agent:基于Java的Attach Api,Agent可以动态附着一个运行的JVM上,然后开启一个BTrace Server,接受Client发过来的BTrace脚本。解析脚本然后根据脚本中的规则找到要修改的类;然后调用Java Instruments的reTransform接口完成对象行为的修改并使之生效。
安全性
- 如上流程最终借助Java instruments 和Java Attach api实现class替换,出于安全考虑,instruments在使用上会有诸多的限制:
- 不允许创建对象
- 不允许创建数组
- 不允许抛异常
- 不允许catch异常
- 不允许随意调用其他对象或者类的方法,只允许调用com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)
- 不允许改变类的属性
- 不允许有成员变量和方法,只允许存在static public void方法
- 不允许有内部类、嵌套类
- 不允许有同步方法和同步块
- 不允许有循环
- 不允许随意继承其他类(当然,java.lang.Object除外)
- 不允许实现接口
- 不允许使用assert
- 不允许使用Class对象
- 如此多的限制,其实可以理解。BTrace要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。
应用
- 应用详见本人git项目
Arthas
- BTrace脚本使用上也有一定学习成本,如果吧常用功能封装好,提供简单命令,那就编程了Archas
最后
- 总结我们之前的知识点,Java的Instruments给运行时的动态追踪留下了一个初始APi,Attach API则给运行时动态追踪提供了“出入口”,ASM则大大方便了“人类”操作Java字节码的操作。
- 所以基于以上JDK api,前辈们创造出来诸如JProfiler, JVisualvm,BTrace,Archas这样的工具。
- 以上大大提高了软件开发人民定位问题的效率。