字节码增强概述
source link: https://segmentfault.com/a/1190000040226596
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
字节码我们都知道是java
文件经过编译之后的class
文件,每一个字节码文件都要由10部分按照固定的顺序组成;增强其实就是对字节码文件进行改造生成一个新的文件,已达到我们的目的,比如动态代理,AOP
等;当然增强完需要能被使用,所以涉及到到加载的问题;在介绍之前我们先来看看都有哪些字节码增强技术。
常见的字节码增强技术大致分为两类:静态增强和动态增强;静态增强最常见的就是AspectJ
了,可以直接编译类,有自己的语法;动态增强包括:ASM
、Javassist
、Cglib
、Java Proxy
;下面分别做简单介绍。
AspectJ
AspectJ
来自于Eclipse
基金会,属于静态织入,主要采用的是编译期织入,在这个期间使用AspectJ
的acj
编译器(类似javac
)把aspect
类编译成class
字节码;下面看一下AspectJ
是如何使用的;
下载安装
AspectJ
官网地址:https://www.eclipse.org/aspectj/
直接下载最新版:AspectJ 1.9.6
;直接运行以下命令即可安装:java -jar aspectj-1.9.6.jar
指定安装目录,然后配置classPath
和path
即可:
ASPECTJ_HOME=E:\aspectj1.9
CLASSPATH=...%ASPECTJ_HOME%\lib\aspectjrt.jar
PATH=...%ASPECTJ_HOME%\bin
编译使用
可以直接使用AspectJ
提供的Demo
进行测试examples\tjp
目录下:
里面有两个java
文件分别是Demo.java
和GetInfo.java
,Demo
就是我们正常的java
文件,而GetInfo
是增强文件,里面有一些AspectJ
语法,需要使用ajc
命令编译E:\aspectj1.9\doc\examples\tjp>ajc -argfile files.lst E:\aspectj1.9\doc\examples\tjp>cd .. E:\aspectj1.9\doc\examples>java tjp.Demo Intercepted message: foo in class: tjp.Demo Arguments: 0. i : int = 1 1. o : java.lang.Object = tjp.Demo@6e3c1e69 Running original method: Demo.foo(1, tjp.Demo@6e3c1e69) result: null Intercepted message: bar in class: tjp.Demo Arguments: 0. j : java.lang.Integer = 3 Running original method: Demo.bar(3) result: Demo.bar(3) Demo.bar(3)
可以发现经过ajc编译后的新class文件,go和bar两个方法都得到了增强,在方法调用前和后、以及方法参数都添加了日志输出;可以发现AspectJ
在运行前就已经对class文件做了增强处理;Spring AOP借鉴了AspectJ
的一些概念,但是在实现上并没有采用AspectJ
而使用动态增强技术;
ASM
是一个通用的Java
字节码操作和分析框架,它可以用来修改现有的类或直接以二进制形式动态生成类;ASM
提供了一些常见的字节码转换和分析算法,从中可以构建定制的复杂转换和代码分析工具;几个核心的类:
- ClassVisitor:用于生成和转换编译类的
ASM API
基于ClassVisitor
抽象类,这个类中的每个方法都对应于同名的类文件结构; - ClassReader:此类主要功能就是读取字节码文件,然后把读取的数据通知
ClassVisitor
; - ClassWriter:其继承于
ClassVisitor
,主要用来生成类;
ASM偏底层字节码操作,所有需要对字节码命令比较熟悉,但是性能高;比如FastJson、Cglib、Lombok等都依赖ASM;大体的操作步骤就是首先需要加载原Class文件,然后通过访问者模式访问所有元素,在访问的过程中对各元素进行改造,最后重新生成一个字节码的二进制文件,根据需求进行加载新Class或者重加载;
更多参考:ASM入门篇
Javassist
正因为ASM
需要对字节码命令熟悉,而字节码本身就比较晦涩难懂,所有就有了更容易理解的增强工具Javassist
,可以直接使用Java编码的方式,无需了解相关字节码指令,对开发者更加友好,当然性能肯定不及ASM
,下面看一下几个常见的类:
- ClassPool:可以简单理解就是存放类的池子,所有
CtClass
都要从池中获取; - CtClass:表示一个类文件,可以通过一个类的全限定名来获取一个
CtClass
对; - CtMethod:对应的类中的方法,可以通过
CtClass
获取指定方法; - CtField:对应类中的属性,可以通过
CtClass
获取指定属性;
结合以上几个核心类看一个简单的日志增强实例:
public class JavassistTest {
public static void main(String[] args) throws Exception {
//增强后的类信息存放路径
CtClass.debugDump = "./dump";
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("com.zh.asm.TestService");
CtMethod m = cc.getDeclaredMethod("query");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
TestService h = (TestService) c.newInstance();
h.query();
}
}
Dubbo
使用Javassist
是做动态编译处理,JBoss
中使用Javassist
来做AOP
处理等;
更多:http://www.javassist.org/tuto...
Cglib
Cglib
是一个功能强大,高性能的代码生成包,底层依赖ASM
;为JDK
的动态代理提供了很好的补充,Java
代理不支持没有接口的情况,另外就是Cglib
的性能更强大;几个核心的类如下:
- Enhancer:
Cglib
中最常用的一个类,和动态代理中引入的Proxy
类类似,不同的是Enhancer
既能够代理普通的class
,也能够代理接口; - MethodInterceptor:拦截器,在调用目标方法时,
CGLib
会回调MethodInterceptor
接口方法拦截,来实现你自己的代理逻辑,类似于JDK
中的InvocationHandler
接口;
public class CgLibProxy {
public static void main(String[] args) {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:/asm/cglib");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestService.class);
enhancer.setCallback(new MyMethodInterceptor());
TestService testService = (TestService)enhancer.create();
testService.query();
}
}
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("增强处理");
Object object = proxy.invokeSuper(obj, args);
return object;
}
}
Cglib会动态生成一个代理类,真正执行的时候其实就是代理类,在代理类里面又调用了原始类,实现功能增强;
Java Proxy
利用反射机制在运行时创建代理类;接口、被代理类不变,构建一个handler
类来实现InvocationHandler
接口;核心类如下:
- Proxy:指定
ClassLoader
对象和一组interface
来创建动态代理类; - InvocationHandler:创建自己的调用处理器,也就是增强处理;
public class MyHandler implements InvocationHandler{
private Object object;
public MyHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke...");
method.invoke(object, args);
System.out.println("After invoke...");
return null;
}
}
通过反射机制获得动态代理类的构造函数,通过构造函数创建动态代理类实例;而Cglib
里面底层依赖ASM
,比直接使用反射性能更强;FastJson
中就是用了ASM
来代替反射的使用,Spring AOP
实现也是使用Cglib
来实现。
以上对各种字节码增强技术做了一个简单的介绍,不管哪种增强技术,增强完之后的类信息都需要被加载,根据使用不同的增强技术,以及增强的作用不同,使用的类加载方式也不一样,大致总结了以下几种情况;
这种情况也就是上面介绍的AspectJ
技术,在运行前就对class
做了增强处理,所以加载方式和普通类的加载方式没有任何区别,在运行期间可能都不知道对class
文件做了增强;这种方式类加载是最简单的;
这种方式使用动态增强技术,创建代理类,代理类有自己的唯一名称,代理类实现接口类或者继承于原始类,其实就是生成了一个新的类,在介绍ASM
的时候也提到它的两项功能:生成类和转换类;FastJson
中使用的也是ASM
的生成类功能;所以这种情况下只要准备一个类加载即可;ASM
没有提供类加载,而其他几种动态增强技术都提供了类加载功能;
这种情况是最复杂的,如果需要被增强的类已经被加载到内存中,如何在字节码增强之后重加载类;instrument
是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持;在JDK 1.6以前,instrument
只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument
支持了在运行时对类定义的修改;具体使用需要提供一个ClassFileTransformer
实现类,实现transform方法,此方法返回一个字节数组,这个字节数组可以使用以上介绍的字节码增强工具来生成;
使用 Instrumentation
,开发者可以构建一个独立于应用程序的代理程序(Agent
),用来监测和协助运行在 JVM
上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP
实现方式,使得开发者无需对 JDK
做任何升级和改动,就可以实现某些 AOP
的功能了。
以上介绍的instrument
机制依赖JPDA
:Java 平台调试架构(Java Platform Debugger Architecture
),它是 Java 虚拟机为调试和监控虚拟机专门提供的一套接口;JPDA
由三个规范组成:JVMTI(JVM Tool Interface)
、JDWP(Java Debug Wire Protocol)
、JDI(Java Debug Interface)
;
更多:https://docs.oracle.com/javas...
本文简单介绍了一下常用的一些字节码增强技术,正是这些底层技术的支持,帮助我们在开发的过程中大大提升开发的效率比如lombok、aop
等;提升性能比如FastJson、ReflectASM
等;配合Java Agent
来进行热更新等等。
可以关注微信公众号「回滚吧代码」,第一时间阅读,文章持续更新;专注Java源码、架构、算法和面试。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK