java.lang.instrument.Instrumentation

 

之前有写 ​​基于AOP的日志调试​​​ 讨论一种跟踪Java程序的方法, 但不是很完美.后来发现了 ​​Btrace​​ , 由于它借助动态字节码注入技术 , 实现优雅且功能强大.
只不过, 用起来总是磕磕绊绊的, 时常为了跟踪某个问题, 却花了大把的时间调试Btrace的脚本. 为此, 我尝试将几种跟踪模式固化成脚本模板, 待用的时候去调整一下正则表达式之类的.
跟踪过程往往是假设与验证的螺旋迭代过程, 反复的用BTrace跟踪目标进程, 总有那么几次莫名其妙的不可用, 最后不得不重启目标进程. 若真是线上不能停的服务, 我想这种方式还是不靠谱啊.
为此, 据决定自己的搞个用起来简单, 又能良好支持反复跟踪而不用重启目标进程的工具.

AOP

AOP是Btrace, jip​1​等众多监测工具的核心思想, 用一段代码最容易说明:

​​public​​            ​​void​​            ​​say(String words){​​           


​​Trace.enter();​​


​​System.out.println(words);​​


​​Trace.exit();​​


​​}​​



如上, Trace.enter() 和 Trace.exit() 将say(words)内的代码环抱起来, 对方法进出的进行切面的处理, 便可获取运行时的上下文, 如:

  • 调用栈
  • 当前线程
  • 时间消耗
  • 参数与返回值
  • 当前实例状态

实现的选择

实现切面的方式, 我知道的有以下几种:

代理(装饰器)模式

设计模式中装饰器模式和代理模式, 尽管解决的问题域不同, 代码实现是非常相似, 均可以实现切面处理, 这里视为等价. 依旧用代码说明:


​​interface​​            ​​Person {​​           


​​void​​ ​​say(String words);​​


​​}​​





​​class​​ ​​Officer ​​ ​​implements​​ ​​Person {​​


​​public​​ ​​void​​ ​​say(String words) { lie(words); }​​


​​private​​ ​​void​​ ​​lie(String words) {...}​​


​​}​​





​​class​​ ​​Proxy ​​ ​​implements​​ ​​Person {​​


​​private​​ ​​final​​ ​​Officer officer;​​


​​public​​ ​​Proxy(Officer officer) { ​​ ​​this​​ ​​.officer = officer; }​​


​​public​​ ​​void​​ ​​say(String words) {​​


​​enter();​​


​​officer.say(words);​​


​​exit();​​


​​}​​


​​private​​ ​​void​​ ​​enter() { ... }​​


​​private​​ ​​void​​ ​​exit() { ... }​​


​​}​​





​​Person p = ​​ ​​new​​ ​​Proxy(​​ ​​new​​ ​​Officer());​​



很明显, 上述enter() 和exit()是实现切面的地方, 通过获取Officer的Proxy实例, 便可对Officer实例的行为进行跟踪. 这种方式实现起来最简单, 也最直接.

Java Proxy

Java Proxy是JDK内置的代理API, 借助反射机制实现. 用它来是完成切面则会是:


​​class​​            ​​ProxyInvocationHandler ​​            ​​implements​​            ​​InvocationHandler {​​           


​​private​​ ​​final​​ ​​Object target;​​


​​public​​ ​​ProxyInvocationHandler(Object target) { ​​ ​​this​​ ​​.target = target;}​​


​​public​​ ​​Object handle(Object proxy, Method method, Object[] args) {​​


​​enter();​​


​​method.invoke(target, args);​​


​​exit();​​


​​}​​


​​private​​ ​​void​​ ​​enter() { ... }​​


​​private​​ ​​void​​ ​​exit() { ... }​​


​​}​​


​​ClassLoader loader = ...​​


​​Class<?>[] interfaces = {Person.​​ ​​class​​ ​​};​​


​​Person p = (Person)Proxy.newInstance(loader, interfaces, ​​ ​​new​​ ​​ProxyInvocationHandler(​​ ​​new​​ ​​Officer()));​​



相比较上一中方法, 这种不太易读, 但更为通用, 对具体实现依赖很少.

AspectJ

AspectJ是基于字节码操作(运行时利用ASM库)的AOP实现, 相比较Java proxy, 它会显得对调用更”透明”, 编写更简明(类似DSL), 性能更好. 如下代码:


​​pointcut say(): execute(* say(..))​​           


​​before(): say() { ... }​​


​​after() : say() { ... }​​



Aspectj实现切面的时机有两种: 静态编译和类加载期编织(load-time weaving). 并且它对IDE的支持很丰富.

CGlib

与AspectJ一样CGlib也是操作字节码来实现AOP的, 使用上与Java Proxy非常相似, 只是不像Java Proxy对接口有依赖, 我们熟知的Spring, Guice之类的IoC容器实现AOP都是使用它来完成的.

​​class​​            ​​Callback ​​            ​​implements​​            ​​MethodInterceptor {​​           


​​public​​ ​​Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) ​​ ​​throws​​ ​​Throwable {​​


​​enter();​​


​​proxy.invokeSuper(obj, args);​​


​​exit();​​


​​}​​


​​private​​ ​​void​​ ​​enter() { ... }​​


​​private​​ ​​void​​ ​​exit() { ... }​​


​​}​​


​​Enhancer e = ​​ ​​new​​ ​​Enhancer();​​


​​e.setSuperclass(Officer.​​ ​​class​​ ​​);​​


​​e.setCallback(​​ ​​new​​ ​​Callback());​​


​​Person p = e.create();​​



字节码操纵

上面四种方法各有适用的场景, 但唯独对运行着的Java进程进行动态的跟踪支持不了, 当然也许是我了解的不够深入, 若有基于上述方案的办法还请不吝赐教.

还是回到Btrace的思路上来, 在理解了它借助java.lang.Instrumentation进行字节码注入的实现原理后, 实现动态变化跟踪方式或目标应该没有问题.

借下来的问题, 如何操作(注入)字节码实现切面的处理. 可喜的是, “构建自己的监测工具”一文给我提供了一个很好的切入点. 在此基础上, 经过一些对ASM的深入研究, 可以实现:

  • 方法调用进入时, 获取当前实例(this) 和 参数值列表;
  • 方法调用出去时, 获取返回值;
  • 方法异常抛出时, 触发回调并获取异常实例.

其切面实现的核心代码如下:


​​private​​            ​​static​​            ​​class​​            ​​ProbeMethodAdapter ​​            ​​extends​​            ​​AdviceAdapter {​​           





​​protected​​ ​​ProbeMethodAdapter(MethodVisitor mv, ​​ ​​int​​ ​​access, String name, String desc, String className) {​​


​​super​​ ​​(mv, access, name, desc);​​


​​start = ​​ ​​new​​ ​​Label();​​


​​end = ​​ ​​new​​ ​​Label();​​


​​methodName = name;​​


​​this​​ ​​.className = className;​​


​​}​​





​​@Override​​


​​public​​ ​​void​​ ​​visitMaxs(​​ ​​int​​ ​​maxStack, ​​ ​​int​​ ​​maxLocals) {​​


​​mark(end);​​


​​catchException(start, end, Type.getType(Throwable.​​ ​​class​​ ​​));​​


​​dup();​​


​​push(className);​​


​​push(methodName);​​


​​push(methodDesc);​​


​​loadThis();​​


​​invokeStatic(Probe.TYPE, Probe.EXIT);​​


​​visitInsn(ATHROW);​​


​​super​​ ​​.visitMaxs(maxStack, maxLocals);​​


​​}​​





​​@Override​​


​​protected​​ ​​void​​ ​​onMethodEnter() {​​


​​push(className);​​


​​push(methodName);​​


​​push(methodDesc);​​


​​loadThis();​​


​​loadArgArray();​​


​​invokeStatic(Probe.TYPE, Probe.ENTRY);​​


​​mark(start);​​


​​}​​





​​@Override​​


​​protected​​ ​​void​​ ​​onMethodExit(​​ ​​int​​ ​​opcode) {​​


​​if​​ ​​(opcode == ATHROW) ​​ ​​return​​ ​​; ​​ ​​// do nothing, @see​​


​​prepareResultBy(opcode);​​


​​push(className);​​


​​push(methodName);​​


​​push(methodDesc);​​


​​loadThis();​​


​​invokeStatic(Probe.TYPE, Probe.EXIT);​​


​​}​​





​​private​​ ​​void​​ ​​prepareResultBy(​​ ​​int​​ ​​opcode) {​​


​​if​​ ​​(opcode == RETURN) { ​​ ​​// void​​


​​push((Type) ​​ ​​null​​ ​​);​​


​​} ​​ ​​else​​ ​​if​​ ​​(opcode == ARETURN) { ​​ ​​// object​​


​​dup();​​


​​} ​​ ​​else​​ ​​{​​


​​if​​ ​​(opcode == LRETURN || opcode == DRETURN) { ​​ ​​// long or double​​


​​dup2();​​


​​} ​​ ​​else​​ ​​{​​


​​dup();​​


​​}​​


​​box(Type.getReturnType(methodDesc));​​


​​}​​


​​}​​





​​private​​ ​​final​​ ​​String className;​​


​​private​​ ​​final​​ ​​String methodName;​​


​​private​​ ​​final​​ ​​Label start;​​


​​private​​ ​​final​​ ​​Label end;​​


​​}​​



更多参考请见这里的 ​​Demo​​ , 它是javaagent, 在伴随宿主进程启动后, 提供MBean可用jconsole进行动态跟踪的管理.

后续的方向

  1. 提供基于Web的远程交互界面;
  2. 提供基于Shell的本地命令行接口;
  3. 提供Profile统计和趋势输出;
  4. 提供跟踪日志定位与分析.

参考

  1. ​The Java Interactive Profiler​
  2. ​Proxy Javadoc​
  3. ​Aspectj​
  4. ​CGlib​
  5. ​BTrace User’s Guide​
  6. ​java动态跟踪分析工具BTrace实现原理​
  7. ​构建自己的监测工具​
  8. ​ASM Guide​
  9. ​常用 Java Profiling 工具的分析与比较​
  10. ​AOP​​​​@Work​​ : Performance monitoring with AspectJ
  11. ​The JavaTM Virtual Machine Specification​
  12. ​来自rednaxelafx的JVM分享​​​, 他的 ​​Blog​
  13. ​BCEL​