官方文档
),这份脚本会跟踪到javax.swing.*包下的所有class下的所有method,并在进入方法体时通过标准输出打印出类名和方法名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package samples;import com.sun.btrace.annotations.*;import static com.sun.btrace.BTraceUtils.*;@BTrace public class AllMethods { @OnMethod( clazz="/javax\\.swing\\..*/", method="/.*/" ) public static void m (@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) { print(Strings.strcat("entered " , probeClass)); println(Strings.strcat("." , probeMethod)); } }
这份例子仅仅是一个简单的例子,btrace追踪点的时机(对应例子里的@OnMethod)可以有很多,包括方法体进入/退出、方法调用与返回、行号、异常抛出、临界区进入和退出等等,追踪的内容(对应例子里的@ProbeClassName、@ProbeMethodName)除了提到的类名和方法名,还有对象的实例、入参和返回值、方法耗时等都可以作为参数注入到脚本方法的入参中。看得出,btrace脚本的语法强大且复杂,但是为了安全(不能修改程序自身逻辑)做了诸多的限制,例如不能新建对象、不能调用实例方法以及静态方法(BTraceUtils等特有方法除外)、不能使用循环、不能抛出和捕获异常等等。
Greys
也是一个Java程序诊断工具(阿里内部叫Arthas,对其做了二次开发)其原理与btrace类似,区别在于用户不需要编写btrace脚本,直接通过命令行指令交互。因此它更像一个产品而不仅仅是工具,它提供了包括方法的出入参监控、类加载信息查看、调用堆栈查看、方法调用轨迹和耗时查看的功能。在实际线上问题诊断中,尤其是在无法debug的环境中定位问题,还是非常实用的。
举个例子,Greys可以以下面这种使用方式来监控某个方法的调用轨迹和内部耗时,参数包括了监控的类名表达式、方法名、追踪的路径表达式等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
ga?>ptrace -t *alibaba*Test printAddress --path=*alibaba* Press Ctrl+D to abort. Affect(class-cnt:10 , method-cnt:36) cost in 148 ms. `---+pTracing for : thread_name="agent-test-address-printer" thread_id=0xb;is_daemon=false;priority=5;process=1004; `---+[2,2ms]com.alibaba.AgentTest:printAddress(); index=1021; +---+[1,1ms]com.alibaba.manager.DefaultAddressManager:newAddress(); index=1014; | +---[1,1ms]com.alibaba.CountObject:<init>(); index=1012; | `---[1,0ms]com.alibaba.Address:<init>(); index=1013; +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toString(); index=1020; | +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toStringPass1(); index=1019; | | +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toStringPass2(); index=1017; | | | +---[1,0ms]com.alibaba.Address:getAddressId(); index=1015; | | | +---+[1,0ms]com.alibaba.manager.DefaultAddressManager:throwRuntimeException(); index=1016; | | | | `---[1,0ms]throw:java.lang.RuntimeException | | | `---[1,0ms]throw:java.lang.RuntimeException | | +---[2,0ms]com.alibaba.AddressException:<init>(); index=1018; | | `---[2,0ms]throw:com.alibaba.AddressException | `---[2,0ms]throw:com.alibaba.AddressException `---[2,0ms]throw:com.alibaba.AddressException
从Greys的原理来看,除了去掉了btrace脚本和Java Complier的部分以外,和btrace基本一样,毕竟都是Instrumentation的实际应用。在一些细节上,例如类加载的隔离还是值得研究学习的,可以直接从开源项目里拉到源码来看。
JRebel
:目前最常用的热部署工具,是一款收费的商业软件,因此在稳定性和兼容性上做的都比较好。
Spring-Loaded
:Spring旗下的子项目,也是一款开源的热部署工具。
Hotcode2:阿里内部开发和使用的热部署工具,功能和上面基本一样,同时针对各种框架做了很多适配。
这类热部署工具的原理惊人的相似:首先都是通过Java agent,使用Instumentation API来修改已加载的类。既然Instumentation只能修改方法体,为什么这些工具突破了这个限制呢?实际上,
这些工具在每个method call和field access的地方都做了一层代理
,对于每次修改类,并不是直接retransformClasses,而是直接加载一个全新的类,由于方法调用和成员变量读写都被动态代理过,新修改的类自然能够成功“篡位”了。
举一个JRebel的简化版的例子,假设一个类一开始长这样:
1 2 3 4 5 6 7 8 9
public class C extends X { int y = 5 ; int method1 (int x) { return x + y; } void method2 (String s) { System.out.println(s); } }
那么这个类在加载时,就会被JRebel的agent转换掉:每个方法的方法体都变成了代理,其内容变成了调用某个具体实现类的同名方法。
1 2 3 4 5 6 7 8 9 10 11 12 13
public class C extends X { int y = 5 ; int method1 (int x) { Object[] o = new Object[1 ]; o[0 ] = x; return Runtime.redirect(this , o, "C" , "method1" , "(I)I" ); } void method2 (String s) { Object[] o = new Object[1 ]; o[0 ] = s; return Runtime.redirect(this , o, "C" , "method2" , "(Ljava/lang/String;)V" ); } }
原代码的实现逻辑当然也不会丢掉,而是通过加载一个名叫C0的新类作为实现类。刚才通过Runtime.redirect的调用,会被路由到这个实现类的对应方法里。如果此时用户再次更新了类C的代码,那么会再重新加载一个C1类,然后C2,C3,C4,C5…
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public abstract class C0 { public static int method1 (C c, int x) { int tmp1 = Runtime.getFieldValue(c, "C" , "y" , "I" ); return x + tmp1; } public static void method2 (C c, String s) { PrintStream tmp1 = Runtime.getFieldValue( null , "java/lang/System" , "out" , "Ljava/io/PrintStream;" ); Object[] o = new Object[1 ]; o[0 ] = s; Runtime.redirect(tmp1, o, "java/io/PrintStream;" , "println" ,"(Ljava/lang/String;)V" ); } }
通过这种方式,就可以在JVM既定的限制下,完成更自由的热部署。当然这种热部署行为,是需要做很多细节的兼容的,例如反射的各个方法都要做一些特殊的兼容处理,还有异常栈的获取不能真的把这些代理类透传出去……另外,由于很多类的行为是通过框架初始化的时候进行的,这些热部署工具还要对一些框架深度加工,来完成xml和注解的自动初始化,比如spring的配置xml、mybatis的sqlmap等。
https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html
https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html
http://www.infoq.com/cn/articles/javaagent-illustrated
http://lovestblog.cn/blog/2014/06/18/jvm-attach/
http://www.jianshu.com/p/b034f5bb6283
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/sun/tools/attach/LinuxVirtualMachine.java
https://zeroturnaround.com/rebellabs/why-hotswap-wasnt-good-enough-in-2001-and-still-isnt-today/