Spring AOP & AspectJ之原理探析

有人可能会问,Spring不是有自己原生的AOP组件吗,为什么还要引入AspectJ呢?

同问,俄罗斯的军工那么牛逼,为什么普京还是要从法国订购西北风级两栖攻击舰呢?

无他,自己的东西不争气,最好的选择就是:恶心自己,成全别人

说起Spring原生的AOP组件,实在有点给Spring大家族丢人,因为使用它需要实现大量的接口,继承大量的类,不优雅而且难用,这与Spring一直秉承的无侵入、低耦合原则大相径庭。Spring 原生的AOP组件大多数也就是撑撑门面的东西,在实际的开发中很少有人用它来实现切面编程,AspectJ才是事实上的AOP标准。

好在Spring对开源的优秀框架向来是采用兼容包并的态度。所以,后来的Spring 就提供了对AspectJ支持,也就是我们后来所说的基于纯POJO的AOP。

不同的是,Spring AOP采用的动态织入,利用其强大的动态代理功能,在程序运行期间将Advice 织入到Jointpoint;而AspectJ采用的是静态织入,它有专门的编译器,将Advice以字节码的形式织入到class文件中。

1、AspectJ与静态织入

AspectJ的下载、安装很简单,在此不再赘述。

安装目录的bin文件夹下包含AspectJ支持的命令:

《Spring AOP & AspectJ之原理探析》

其中 “ajc” 命令最常用,它的作用类似于“javac”,用于对普通 Java 类进行编译时增强。

事务管理与日志记录是开发中经常涉及的功能,而且很适合通过切面来实现。下面通过一个简单的例子来模拟这两个流程。

SampleService用于模拟一个业务组件

public class SampleService{
 
      public void doBusiness(){
             System.out.println("执行业务逻辑");
      }
 
}

日志切面:

public aspect LogAspect{
 
      before():execution(void*.doBusiness()){
             System.out.println("---模拟记录日志");
      }
 
}

事务切面:

public aspect TransactionAspect{
 
      voidaround():execution(void *.doBusiness()){
             System.out.println("---模拟事务开始");
             proceed();  //AspectJ的回调函数
             System.out.println("---模拟事务结束");
      }
 
}

还有一个启动类,调用SampleService的doBusiness()方法:

public class Main{
      public static voidmain(String[] args) {
             SampleServicesampleService = new SampleService();
             sampleService.doBusiness();
      }
}

切面类当中,用到了AspectJ的特有语法,实际上跟切入点表达式没什么两样。日志用到的前置通知,事务用到的是环绕通知,两个通知都会织入到任何名为doBusiness()的方法当中。

将这四个类放在一个文件夹下面,运行命令:

ajc -d. *.java 


我们可以把 ajc.exe理解成 javac.exe 命令,都用于编译 Java 程序,区别是ajc.exe 命令可识别 AspectJ的语法;从这个意义上看,我们可以将ajc.exe 当成一个增强版的javac.exe 命令。

ajc命令会将以上四个类编译成.class文件:

《Spring AOP & AspectJ之原理探析》

此时,用”javaMain” 命令启动程序:

《Spring AOP & AspectJ之原理探析》

可见,日志记录与实务管理已经织入到SampleService当中了。

使用反编译工具查看SampleService.class、TransactionAspect.class、LogAspect.class三个文件,一探究竟:

import java.io.PrintStream;
import org.aspectj.runtime.internal.AroundClosure;
 
public class SampleService {
      public void doBusiness(){  //该方法只是保留了方法名,内部实现已经被AspectJ改造
             LogAspect.aspectOf().ajc$before$LogAspect$1$a18068bd();  //执行日志切面逻辑
             doBusiness_aroundBody1$advice(this,TransactionAspect.aspectOf(), null);
      }
     
      //原来的doBusiness()逻辑被移到了这个方法当中
      private static final voiddoBusiness_aroundBody0(SampleService ajc$this) { 
             System.out.println("执行业务逻辑");
      }
 
      private static final voiddoBusiness_aroundBody1$advice(SampleService ajc$this,
                    TransactionAspectajc$aspectInstance, AroundClosure ajc$aroundClosure) {
             System.out.println("---模拟事务开始");
             AroundClosurelocalAroundClosure = ajc$aroundClosure;
             doBusiness_aroundBody0(ajc$this);  //执行核心业务逻辑,语句前后加入了TransactionAspect的切面代码
             System.out.println("---模拟事务结束");
      }
}
 
import java.io.PrintStream;
import org.aspectj.lang.NoAspectBoundException;
import org.aspectj.runtime.internal.AroundClosure;
 
public class TransactionAspect {
      private static Throwableajc$initFailureCause;
      public static finalTransactionAspect ajc$perSingletonInstance;
 
      static {
             try {
                    ajc$postClinit();  //事务切面类在静态代码块中被初始化,单例模式
             } catch (ThrowablelocalThrowable) {
                    ajc$initFailureCause= localThrowable;
             }
      }
 
      public voidajc$around$TransactionAspect$1$a18068bd(AroundClosure ajc$aroundClosure) {
             System.out.println("---模拟事务开始");
             ajc$around$TransactionAspect$1$a18068bdproceed(ajc$aroundClosure);  //调用被目标方法
             System.out.println("---模拟事务结束");
      }
 
      static voidajc$around$TransactionAspect$1$a18068bdproceed(AroundClosure this) throwsThrowable {
      }
 
      public staticTransactionAspect aspectOf() {  //获取单例
             if(ajc$perSingletonInstance == null)
                    throw newNoAspectBoundException("TransactionAspect", ajc$initFailureCause);
             returnajc$perSingletonInstance;
      }
 
      public static booleanhasAspect() {
             return(ajc$perSingletonInstance != null);
      }
 
      private static voidajc$postClinit() {
             ajc$perSingletonInstance= new TransactionAspect();
      }
}
 
import java.io.PrintStream;
import org.aspectj.lang.NoAspectBoundException;
 
public class LogAspect {
      private static Throwableajc$initFailureCause;
      public static finalLogAspect ajc$perSingletonInstance;
 
      static {
             try {
                    ajc$postClinit();  //日志切面类在静态代码块中被初始化,单例模式
             } catch (ThrowablelocalThrowable) {
                    ajc$initFailureCause= localThrowable;
             }
      }
 
      public voidajc$before$LogAspect$1$a18068bd() {  //执行切面逻辑
             System.out.println("---模拟记录日志");
      }
 
      public static LogAspectaspectOf() {  //获取单例
             if(ajc$perSingletonInstance == null)
                    throw newNoAspectBoundException("LogAspect", ajc$initFailureCause);
             returnajc$perSingletonInstance;
      }
 
      public static booleanhasAspect() {
             return(ajc$perSingletonInstance != null);
      }
 
      private static voidajc$postClinit() {
             ajc$perSingletonInstance= new LogAspect();
      }
}

不出所料,AspectJ的编译器果然对代码动了手脚,相当于把TransactionAspect与LogAspect的代码整合到了SampleService当中,这就是所谓的静态织入。如此一来,虚拟机加载SampleService.class时,加载的就是改造后的class,运行时调用该服务,切面逻辑自然包括其中。

与 AspectJ 相对的还有另外一种 AOP 框架,它们不需要在编译时对目标类进行增强,而是运行时生成目标类的代理类,该代理类要么与目标类实现相同的接口,要么是目标类的子类——总之,代理类的实例可作为目标类的实例来使用。一般来说,编译时增强的 AOP 框架在性能上更有优势——因为运行时动态增强的 AOP 框架需要每次运行时都进行动态增强。

2、Spring AOP与动态织入

与 AspectJ 相同的是,Spring AOP 同样需要对目标类进行增强,也就是生成新的 AOP 代理类;与 AspectJ不同的是,Spring AOP 无需使用任何特殊命令对 Java 源代码进行编译,它采用运行时动态地、在内存中临时生成“代理类”的方式来生成 AOP 代理。

Spring 允许使用 AspectJAnnotation 用于定义切面(Aspect)、切入点(Pointcut)和通知(Advice),Spring 框架则可识别并根据这些Annotation 来生成 AOP 代理。Spring 只是使用了和AspectJ 一样的注解,但并没有使用AspectJ 的编译器或者织入器(Weaver),底层依然使用的是 Spring AOP,依然是在运行时动态生成AOP 代理,并不依赖于AspectJ 的编译器或者织入器。

简单地说,Spring 依然采用运行时生成动态代理的方式来增强目标对象,所以它不需要增加额外的编译,也不需要 AspectJ 的织入器支持;而AspectJ 在采用编译时增强,所以AspectJ 需要使用自己的编译器来编译Java 文件,还需要织入器。

在Spring容器中对SampleService织入日志与事务管理逻辑,代码:

import org.springframework.stereotype.Service;
 
//核心业务类
@Service("sampleService")
public class SampleService{
     
      public void doBusiness(){
             System.out.println("执行业务逻辑");
      }
 
}
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
//日志切面
@Component
@Aspect
public class LogAspect {
 
      @Before(value ="execution(void *.doBusiness())")
      public void log() {
             System.out.println("---模拟记录日志");
      }
 
}
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
 
//事务管理切面
@Component
@Aspect
public class TransactionAspect {
 
      @Around(value ="execution(void *.doBusiness())")
      public voiddoTransaction(ProceedingJoinPoint pjp) throws Throwable {
             System.out.println("---模拟事务开始");
             pjp.proceed();
             System.out.println("---模拟事务结束");
      }
 
}
 
//启动类:
@ComponentScan(basePackages ={"clf.learning.winner.springbase"})
@EnableAspectJAutoProxy
public class SpringBaseApp {
     
      public static void main(String [] args) throws Exception {
            
             ApplicationContextcontext = new AnnotationConfigApplicationContext(SpringBaseApp.class);
             SampleServicesampleService = context.getBean(SampleService.class);
             System.out.println("SampleService实现类: " + sampleService.getClass().getSimpleName());
             sampleService.doBusiness();
            
      }
     
}

运行程序,打印结果如下:

《Spring AOP & AspectJ之原理探析》

可见,虽然调用的是SampleService类的doBusiness()方法,但实际执行操作的却是另外一个名为” SampleService$ $EnhancerBySpringCGLIB$ $b74397e3”的类,这个类也就是 Spring AOP 动态生成的 AOP 代理类,从类名可以看出是由CGLIB来生成的。与AspectJ的静态织入不同,Spring AOP的动态织入过程不会对SampleService.class字节码做任何改动,而是将CGLIB生成的动态代理类置于内存当中。

2.1 CGLIB动态代理

CGLIB(Code Generation Library)底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy和BeanShell)也使用ASM生成字节码。

下面通过CGLib来实现对SampleService的动态代理,模拟一下SpringAOP的底层实现。

import java.lang.reflect.Method;
 
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
public class TransactionInterceptor implements MethodInterceptor{
     
      @Override
      public Objectintercept(Object obj, Method method, Object[] args, MethodProxy proxy) throwsThrowable {
             //入参分别代表:动态代理对象、被拦截的方法、方法入参、代理方法对象
            
             System.out.println("---模拟事务开始");
             Object result = proxy.invokeSuper(obj,args);  //调用被代理对象的方法
             System.out.println("---模拟事务结束");
             return result;
      }
     
      public static void main(String[] args) {
             Enhancer enhancer= new Enhancer();
             enhancer.setSuperclass(SampleService.class);  //设置被代理对象
             enhancer.setCallback(new TransactionInterceptor());  //设置方法拦截器,等同于AOP中的Advice
            
             SampleService sampleService = (SampleService)enhancer.create();  //生成动态代理
             sampleService.doBusiness();
      }
 
}

打印结果如下:

《Spring AOP & AspectJ之原理探析》

从上面输出结果来看,CGLIB生成的代理完全可以作为 SampleService对象来使用,这个 CGLIB 代理其实就是 SpringAOP 所生成的 AOP 代理。

代理对象的生成过程由Enhancer类实现,大概步骤如下:

1、生成代理类Class的二进制字节码;

2、通过Class.forName加载二进制字节码,生成Class对象;

3、通过反射机制获取实例构造,并初始化代理类对象。

这就是 SpringAOP 的根本所在:SpringAOP 就是通过 CGLIB 来动态地生成代理对象,这个代理对象就是所谓的 AOP 代理,而 AOP 代理的方法则通过在目标对象的切入点动态地织入增强处理,从而完成了对目标方法的增强。

2.2 JDK动态代理

将上面程序程序稍作修改,让业务逻辑类 SampleService实现任意一个接口,看看有什么变化:

public interface BaseService {
     
      void doBusiness();
     
}
 
import org.springframework.stereotype.Service;
 
@Service("sampleService")
public class SampleService implements BaseService{
     
      @Override
      public void doBusiness(){
             System.out.println("执行业务逻辑");
      }
 
}

运行程序:

《Spring AOP & AspectJ之原理探析》

可见,此时的 AOP 代理并不是由 CGLIB 生成的,而是由 JDK 动态代理生成的。

下面通过JDK自带的工具来实现对SampleService的动态代理。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
public class TransactionHandler implements InvocationHandler {
 
      private Object target; //被代理的对象
 
      public TransactionHandler(Object target) {
             this.target =target;
      }
 
      @Override
      public Objectinvoke(Object proxy, Method method, Object[] args) throws Throwable {
             // 入参分别代表:动态代理对象、被拦截的方法、方法入参
             System.out.println("---模拟事务开始");
             Object result =method.invoke(target, args); // 调用被代理对象的方法
             System.out.println("---模拟事务结束");
             return result;
      }
 
      public static void main(String[] args) {
             BaseService sampleService = new SampleService();
             TransactionHandler transactionHandler = new TransactionHandler(sampleService);
 
             ClassLoader loader= sampleService.getClass().getClassLoader();
             Class[] interfaces= sampleService.getClass().getInterfaces();
 
             // 实例化代理类
             BaseService proxyService = (BaseService) Proxy.newProxyInstance(loader, interfaces,transactionHandler);
             System.out.println("BaseService实现类: " + proxyService.getClass().getSimpleName());
             proxyService.doBusiness();
      }
 
}

打印结果如下:

《Spring AOP & AspectJ之原理探析》

InvocationHandler是JDK动态代理的核心接口,可以看作CGLIB低配版的MethodInterceptor。Proxy是JDK提供的静态工具类,功能类似于CGLIB的Enhancer,用于实例化代理类。

Proxy.newProxyInstance方法会做如下几件事:

1、根据传入的第二个参数interfaces动态生成一个类,实现interfaces中的接口,该例中即BaseService接口的processBusiness方法。并且继承了Proxy类,重写了hashcode,toString,equals等三个方法。

2、通过传入的第一个参数classloder将刚生成的类(即$Proxy0类)加载到jvm中。

3、利用第三个参数,调用$Proxy0的$Proxy0(InvocationHandler)构造函数创建$Proxy0的对象,并且用interfaces参数遍历其所有接口方法,生成Method对象。

2.3 两中动态代理的区别

JDK动态代理与CGLIB动态代理有什么区别呢?

JDK动态代理只能对实现了接口的类生成代理,而CGLIB则没有这项要求。

JDK动态代理是通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法。

CGLIB动态代理是通过继承业务类,生成的动态代理类是业务类的子类,通过重写业务方法进行代理。但因为采用的是继承,所以CGLIB不能对final修饰的类或方法进行代理

Spring AOP 框架对 AOP 代理类的处理原则是:如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。

    原文作者:AOP
    原文地址: https://blog.csdn.net/u012152619/article/details/78756735
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞