谈起AOP就不得不说起代理,Java 源代码经过编译生成字节码,然后再由 JVM 经过类加载,连接,初始化成 Java 类型,可以看到字节码是关键,静态和动态的区别就在于字节码生成的时机
静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在编译时已经将接口,被代理类(委托类),代理类等确定下来,在程序运行前代理类的.class文件就已经存在了
动态代理:在程序运行后通过反射创建生成字节码再由 JVM 加载而成
AOP通常叫面向切面编程(Aspect-oriented Programming,简称AOP),它是一种编程范式,通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术,通常用来对隔离不同业务逻辑,比如常见的事务管理、日志管理等
AOP核心概念
- 切面(Aspect):似于 Java 中的类声明,常用于应用中配置事务或者日志管理。一般使用 @Aspect 注解或者 来定义一个切面
- 连接点(Join Point):程序执行中的特定点,比如方法执行、处理一个异常等
- 切点(Pointcut):通过一种规则匹配的正则表达式,当有连接点可以匹配到切点时,就会触发改切点相关联的指定通知
- 通知(Advice):在切面中某个连接点采取的动作,通知方式也有5种
- around(环绕通知):前后都加
- before(前置通知)
- after(后置通知)
- exception(异常通知)
- return(返回通知)
- 织入(Weaving):链接切面和目标对象创建一个通知对象的过程
静态代理
静态代理的实现代码在这里做不做赘述,简单来讲就是委托类和代理类实现同一个接口,代理类用于委托类的增强
- 静态代理主要有两大劣势
- 代理类只代理一个委托类(其实可以代理多个,但不符合单一职责原则),也就意味着如果要代理多个委托类,就要写多个代理(别忘了静态代理在编译前必须确定)
- 第一点还不是致命的,再考虑这样一种场景:如果每个委托类的每个方法都要被织入同样的逻辑,比如说我要计算前文提到的每个委托类每个方法的耗时,就要在方法开始前,开始后分别织入计算时间的代码,那就算用代理类,它的方法也有无数这种重复的计算时间的代码
动态代理
JDK代理
由于动态代理是程序运行后才生成的,哪个委托类需要被代理到,只要生成动态代理即可,避免了静态代理那样的硬编码,另外所有委托类实现接口的方法都会在 Proxy 的 InvocationHandler.invoke() 中执行,这样如果要统计所有方法执行时间这样相同的逻辑,可以统一在 InvocationHandler 里写, 也就避免了静态代理那样需要在所有的方法中插入同样代码的问题,代码的可维护性极大的提高了
原理:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
- loader:代理类的ClassLoader,最终读取动态生成的字节码,并转成 java.lang.Class 类的一个实例(即类),通过此实例的 newInstance() 方法就可以创建出代理的对象
- interfaces: 委托类实现的接口,JDK 动态代理要实现所有的委托类的接口
- InvocationHandler:委托对象所有接口方法调用都会转发到 InvocationHandler.invoke(),在 invoke() 方法里我们可以加入任何需要增强的逻辑 主要是根据委托类的接口等通过反射生成的
实现:
public class RealSubject implements Subject{
@Override
public void request() {
System.out.println("卖房");
}
}
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始");
method.invoke(target, args);
System.out.println("结束");
return proxy;
}
});
}
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
System.out.println(realSubject.getClass());
Subject subject = (Subject) new ProxyFactory(realSubject).getProxyInstance();
System.out.println(subject.getClass());
subject.request();
}
}
结果输出:
代理类的 class 为 com.sun.proxy.$Proxy0,Proxy 是在 java.lang.reflect 反射包下的,Proxy 的 newProxyInstance 签名
由于动态代理是程序运行后才生成的,哪个委托类需要被代理到,只要生成动态代理即可,避免了静态代理那样的硬编码,另外所有委托类实现接口的方法都会在 Proxy 的 InvocationHandler.invoke() 中执行,这样如果要统计所有方法执行时间这样相同的逻辑,可以统一在 InvocationHandler 里写, 也就避免了静态代理那样需要在所有的方法中插入同样代码的问题,代码的可维护性极大的提高了
CGLIB代理
既然JDK动态代理听起来没问题为什么Spring AOP要使用CGLIB 动态代理呢?
JDK 动态代理虽好,但也有弱点,我们注意到 newProxyInstance 的方法签名,注意第二个参数 Interfaces 是委托类的接口,是必传的, JDK 动态代理是通过与委托类实现同样的接口,然后在实现的接口方法里进行增强来实现的,这就意味着如果要用 JDK 代理,委托类必须实现接口,这样的实现方式看起来有点蠢,更好的方式是什么呢,直接继承自委托类不就行了(superClass),这样委托类的逻辑不需要做任何改动,CGlib 就是这么做的
原理:
开头我们提到的 AOP 就是用的 CGLib 的形式来生成的,JDK 动态代理使用 Proxy 来创建代理类,增强逻辑写在 InvocationHandler.invoke() 里,CGlib 动态代理也提供了类似的 Enhance 类,增强逻辑写在 MethodInterceptor.intercept() 中,也就是说所有委托类的非 final 方法都会被方法拦截器拦截
实现:
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("目标类增强前");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("目标增强后");
return object;
}
public static void main(String[] args) {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
enhancer.setSuperclass(RealSubject.class);
//设置回调函数
enhancer.setCallback(new MyMethodInterceptor());
//这里的creat方法就是正式创建代理类
RealSubject proxyDog = (RealSubject) enhancer.create();
System.out.println(proxyDog.getClass());
//调用代理类的eat方法
proxyDog.request();
}
}
结果输出:
它并不要求委托类实现任何接口,而且 CGLIB 是高效的代码生成包,底层依靠 ASM(开源的 java 字节码编辑类库)操作字节码实现的,性能比 JDK 强,所以 Spring AOP 最终使用了 CGlib 来生成动态代理,只能代理委托类中任意的非 final 的方法,另外它是通过继承自委托类来生成代理的,所以如果委托类是 final 的,就无法被代理了(final 类不能被继承)
总结:
JDK 动态代理的拦截对象是通过反射的机制来调用被拦截方法的
CGlib动态代理通过什么机制来提升了方法的调用效率:由于反射的效率比较低,所以 CGlib 采用了FastClass 的机制来实现对被拦截方法的调用。FastClass 机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法