可以用于在类加载的时候,修改字节码。
Java agent(Java探针)技术
利用javaAgent和ASM字节码技术开发java探针工具,实现原理如下:
jdk1.5以后引入了javaAgent技术,javaAgent是运行方法之前的拦截器。我们利用javaAgent和ASM字节码技术,在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时,同时将方法耗时及内部调用情况放入处理器,处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹和入参map输出到文件中,然后根据map中相应参数或耗时方法轨迹中的关键代码区分出我们要抓取的耗时业务。最后将相应耗时轨迹文件取下来,转化为xml格式并进行解析,通过浏览器将代码分层结构展示出来,方便耗时分析。
原理
Java对象的方法、函数存放在方法区。方法区中的数据是类加载时从class文件中提取出来的。class文件是我们写的文件编译而来。
我们可以在加载class字节码文件时,动态修改这个文件,重新加载class文件。但是不改变对象的属性,也不影响已经存在对象的状态。
java.lang.instrument.Instrumentation
Instrumentation接口,里面有2个方法:redefineClasses和retransformClasses。
一个是重新定义class,一个是修改class。这两个大同小异,都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。
当然在运行时直接替换很不安全。比如新引用不安全的类,删除某个属性,这些情况会发生异常,所以Instrumentation有很多限制,但是在方法前后加日志倒是够了。
现在,我们要修改字节码class文件。当然,最简单的是把修改后的Java文件重新编译一遍得到class文件,然后调用redefineClasses替换。但是,很多时候,我们拿不到这个源码文件。
ASM
我们都知道,Spring的AOP是基于动态代理实现的,Spring会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。那么,Spring是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring在运行时会根据需要动态地创造出一个类,这里创造的过程并非通过字符串写Java文件,然后编译成class文件,然后加载。Spring会直接“创造”一个class文件,然后加载,创造class文件的工具,就是ASM了。
Attach API
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。
Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
Attach API说明
相对于jdk5只能通过启动脚本添加javaagent的方式植入代理,jdk6的动态attach也只是免去了修改启动脚本和不用重启的工作,并没有添加其它新的特性,即使不使用动态attach而是使用脚本添加javaagent的方式也可以达到随时修改class定义的目的,无论通过什么方式我们只要获取了 Instrumentation 实例,然后调用其addTransformer方法添加类转换器再调用retransformClasses就可以转换一个类的字节序列了,这里需要注意的是retransformClasses是jdk1.6定义的,jdk1.5只能使用redefineClasses,retransformClasses功能强大使用简单,但有不能修改方法签名,只能修改body等约束,redefineClasses则是一个可定细节制化的选择。
BTrace
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也不例外。
Arthas
BTrace脚本在使用上有一定的学习成本,如果能把一些常用的功能封装起来,对外直接提供简单的命令即可操作的话,那就再好不过了。阿里的工程师们早已想到这一点,就在去年(2018年9月份),阿里巴巴开源了自己的Java诊断工具——Arthas。Arthas提供简单的命令行操作,功能强大。究其背后的技术原理,和本文中提到的大致无二。Arthas的文档很全面,想详细了解的话可以戳这里。
另外:Javassist
动态编程
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式:
反射:原理也就是通过在运行时获得类型信息然后做相应的操作。
动态编译:动态编译是从Java 6
开始支持的,主要是通过一个JavaCompiler
接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。
调用JavaScript引擎:Java 6
加入了对Script(JSR223)
的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。
动态生成字节码:这种技术通过操作Java字节码的方式在JVM
中生成新类或者对已经加载的类动态添加元素。
动态生成字节码
操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit 。原理也都是利用了Java的设计原理:存在一个虚拟机执行字节码。这就使我们在此处有了改变字节码的操作空间。
ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。
Javassist: 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM。