Spring源碼系列(三)--spring-aop的基礎組件、架構和使用


簡介

前面已經講完 spring-bean( 詳見Spring ),這篇博客開始攻克 Spring 的另一個核心模塊--spring-aop。

顧名思義,spring-aop 是用來做 AOP 開發的,搭配 spring-bean 一起使用的話,AOP 將更加解耦、方便。在實際項目中,spring-aop 被廣泛用來實現日志、權限、事務、異常等的統一管理。

我將通過兩篇博客來詳細介紹 spring-aop 的使用、源碼等。這是第一篇博客,主要介紹 spring-aop 的組件、架構、使用等。

項目環境

maven:3.6.3

操作系統:win10

JDK:8u231

Spring:5.2.6.RELEASE

幾個重要的組件

說到 spring-aop,我們經常會提到JoinpointAdvicePointcutAspectAdvisor等等概念,它們都是抽象出來的“標准”,有的來自 aopalliance,有的來自 AspectJ,也有的是 spring-aop 原創。

它們是構成 spring-aop “設計圖”的基礎,理解它們非常難,一個原因是網上能講清楚的不多,第二個原因是這些組件本身抽象得不夠直觀(spring 官網承認了這一點)。

對Joinpoint做Advice

在 spring-aop 的包中內嵌了 aopalliance 的包(aopalliance 就是一個制定 AOP 標准的聯盟、組織),這個包是 AOP 聯盟提供的一套“標准”,提供了 AOP 一些通用的組件,包的結構大致如下。

└─org
    └─aopalliance
        ├─aop
        │      Advice.class
        │      AspectException.class
        │
        └─intercept
                ConstructorInterceptor.class
                ConstructorInvocation.class
                Interceptor.class
                Invocation.class
                Joinpoint.class
                MethodInterceptor.class
                MethodInvocation.class

使用 UML 表示以上類的關系,如下。可以看到,這主要包含兩個部分:JoinpointAdvice(這是 AOP 最核心的兩個概念)。完整的 aopalliance 包,除了 aop 和 intercept,還包括了 instrument 和 reflect,后面這兩個部分 spring-aop 沒有引入,這里就不說了。

AopAopallianceUML

  1. Joinpoint

Joinpoint表示對某個方法(構造方法或成員方法)或屬性的調用

例如,我調用了 user.save() 方法,這個調用動作就屬於一個JoinpointJoinpoint是一個“動態”的概念,FieldMethodConstructor等對象是它的靜態部分。

如上圖所示,JoinpointAdvice操作的對象

在 spring-aop 中,主要使用Joinpoint的子接口--MethodInvocation

  1. Advice

Joinpoint執行的某些操作

例如,JDK 動態代理使用的InvocationHandler、cglib 使用的MethodInterceptor,在抽象概念上可以算是Advice(即使它們沒有繼承Advice)。

在 spring-aop 中,主要使用Advice的子接口--MethodInterceptor

為了更好地理解這兩個概念,我再舉一個例子:當我們對用戶進行新增操作前,需要進行權限校驗。其中,調用 user.save() 的動作就是一個的Joinpoint,權限校驗就是一個Advice,即對Joinpoint(新增用戶的動作)做Advice(權限校驗)。

在 spring-aop 中,Joinpoint對象持有了一條 Advice chain ,調用Joinpointproceed()方法將采用責任鏈的形式依次執行各個 Advice(注意,Advice的執行可以互相嵌套,不是單純的先后順序)。cglib 和 JDK 動態代理的缺點就在於,它們沒有所謂的 Advice chain,一個Joinpoint一般只能分配一個Advice,當需要使用多個Advice時,需要像套娃一樣層層代理。

其他的幾個概念

在 spring-aop 中,還會使用到其他的概念,例如Advice FilterAdvisorPointcutAspect等。這些概念的重要性不如JoinpointAdvice,如果對深入了解 spring-aop 不感興趣的話,可以不用了解。

  1. Advice Filter

Advice Filter 一般和Advice綁定,它用來告訴我們,Advice是否作用於指定的Joinpoint,如果 true,則將Advice加入到當前JoinpointAdvice chain,如果為 false,則不加入。

在 spring-aop 中,常用的 Advice Filter 包括ClassFilterMethodMatcher,前者過濾的是類,后者過濾的是方法。

  1. Pointcut

Pointcut是 AspectJ 的組件,它是一種 Advice Filter

在 spring-aop 中,Pointcut=ClassFilter+MethodMatcher+。

  1. Advisor

Advisor是 spring-aop 原創的組件,一個 Advisor = 一個 Advice Filter + 一個 Advice

在 spring-aop 中,主要有兩種AdvisorIntroductionAdvisorPointcutAdvisor。前者為ClassFilter+Advice,后者為Pointcut+Advice

  1. Aspect

Aspect也是 AspectJ 的組件,一組同類的PointcutAdvisor的集合就是一個Aspect

在下面代碼中,printRequest 和 printResponse 都是Advice,genericPointCut 是Pointcut,printRequest + genericPointCut 是PointcutAdvisor,UserServiceAspect 是Aspect

@Aspect
public class UserServiceAspect {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceAspect.class);
    
    @Pointcut("execution(* cn.zzs.spring.UserService+.*(..)))")
    public void genericPointCut() {

    }
    
    @Before(value = "genericPointCut()")
    public void printRequest(JoinPoint joinPoint) throws InterruptedException {
        //······
    }  
    
    @After(value = "genericPointCut()")
    public void printResponse(JoinPoint joinPoint) throws InterruptedException {
      //······;
    }  
}

spring-aop 和 AspectJ 的關系

從 AOP 的功能完善程度來講,AspectJ 支持編譯期織入、編譯后織入和類加載時織入,並且提供了一套 AspectJ 語法,非常強大。在 AspectJ 面前,spring-aop 就是個“小弟弟”。

spring-aop 之所以和 AspectJ 產生關聯,主要是因為借鑒了 AspectJ 語法(這套語法一般使用注解實現,用於定義AspectPointcutAdvice等),包括使用到 AspectJ 的注解以及解析語法的類。如果我們希望在 spring-aop 中使用 AspectJ 注解語法,需要額外引入 aspectjweaver 包。

如何使用 spring-aop

接下來展示的代碼可能有的人看了會覺得奇怪,怎么和我平時用 spring-aop 不一樣呢?。這里先說明一點,因為本文講的是 spring-aop,所以,我用的都是 spring-aop 原生的 API,而實際項目中,由於 spring 封裝了一層又一層,導致我們感知不到 spring-aop 的存在。

通常情況下,Spring 是通過向BeanFactory注冊BeanPostProcessor(例如,AbstractAdvisingBeanPostProcessor)的方式對 bean 使用 spring-aop 的功能,原理並不復雜,相關內容可以通過 spring-bean 了解( Spring源碼系列(二)--bean組件的源碼分析 )。

接下來讓我們拋開這些“高級封裝”,看看 spring-aop 的真面目。

spring-aop 的代理工廠

下面通過一個 UML 圖來了解下 spring-aop 的結構,如下。

ProxyFactoryUML

spring-aop 采用動態代理為目標類生成一個代理對象,Joinpoint 的組裝和 advice chain 的執行都是在這個代理對象中完成,而不是通過層層代理的方式來實現

spring-aop 為我們提供了三種代理工廠,其中ProxyFactory比較普通,AspectJProxyFactory支持 AspectJ 語法的代理工廠,ProxyFactoryBean可以給 Spring IoC 管理的 bean 進行代理。

下面介紹如何使用這些代理工廠來獲得代理類。

使用ProxyFactory生成代理類

ProxyFactory的測試代碼如下,如果指定了接口,一般會使用 JDK 動態代理,否則使用 cglib。

    @Test
    public void test01() {
    
        ProxyFactory proxyFactory = new ProxyFactory();
        
        // 設置被代理的對象
        proxyFactory.setTarget(new UserService());
        // 設置代理接口--如果設置了接口,一般會使用JDK動態代理,否則用cglib
        // proxyFactory.setInterfaces(IUserService.class);
        
        // 添加第一個Advice
        proxyFactory.addAdvice(new MethodInterceptor() {
            
            public Object invoke(MethodInvocation invocation) throws Throwable {
                TimeUnit.SECONDS.sleep(1);
                
                LOGGER.info("打印{}方法的日志", invocation.getMethod().getName());
                // 執行下一個Advice
                return invocation.proceed();
            }
        });
        
        // 添加第二個Advice······
        
        IUserService userController = (IUserService)proxyFactory.getProxy();
        userController.save();
        userController.delete();
        userController.update();
        userController.find();
    }

運行以上方法,可以看到控制台輸出:

2020-09-12 16:32:02.704 [main] INFO  cn.zzs.spring.ProxyFactoryTest - 打印save方法的日志
增加用戶
2020-09-12 16:32:03.725 [main] INFO  cn.zzs.spring.ProxyFactoryTest - 打印delete方法的日志
刪除用戶
2020-09-12 16:32:04.726 [main] INFO  cn.zzs.spring.ProxyFactoryTest - 打印update方法的日志
修改用戶
2020-09-12 16:32:05.726 [main] INFO  cn.zzs.spring.ProxyFactoryTest - 打印find方法的日志
查找用戶

使用ProxyFactoryBean生成代理類

ProxyFactoryBeanProxyFactory差不多,區別在於ProxyFactoryBean的 target 是一個 bean。因為需要和 bean 打交道,所以這里需要創建 beanFactory 以及注冊 bean。另外,我們可以設置每次生成的代理類都不同。

    @Test
    public void test01() {
        // 注冊bean
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        beanFactory.registerBeanDefinition("userService", 
                BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition());
        
        ProxyFactoryBean proxyFactory = new ProxyFactoryBean();
        // 設置beanFactory
        proxyFactory.setBeanFactory(beanFactory);
        // 設置被代理的bean
        proxyFactory.setTargetName("userService");
        // 添加Advice
        proxyFactory.addAdvice(new MethodInterceptor() {
            
            public Object invoke(MethodInvocation invocation) throws Throwable {
                TimeUnit.SECONDS.sleep(1);
                
                LOGGER.info("打印{}方法的日志", invocation.getMethod().getName());
                return invocation.proceed();
            }
        });
        // 設置scope
        //proxyFactory.setSingleton(true);
        proxyFactory.setSingleton(false);
        
        IUserService userController = (IUserService)proxyFactory.getObject();
        
        userController.save();
        userController.delete();
        userController.update();
        userController.find();
        
        IUserService userController2 = (IUserService)proxyFactory.getObject();
        System.err.println(userController == userController2);
    }

使用AspectJProxyFactory生成代理類

使用AspectJProxyFactory要額外引入 aspectjweaver 包,如下:

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
            <!-- <scope>runtime</scope> -->
        </dependency>

接下來配置一個Aspect,如下。這里定義了一個Advice,即 printRequest 方法;定義了一個Pointcut,即攔截UserService及其子類。

@Aspect
public class UserServiceAspect {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceAspect.class);
    
    @Pointcut("execution(* cn.zzs.spring.UserService+.*(..)))")
    public void genericPointCut() {

    }
    
    @Before(value = "genericPointCut()")
    public void printRequest(JoinPoint joinPoint) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        LOGGER.info("call {}_{} with args:{}", 
                joinPoint.getSignature().getDeclaringType().getSimpleName(), 
                joinPoint.getSignature().getName(), 
                joinPoint.getArgs());
    }  
}

編寫生成代理類的方法,如下。AspectJProxyFactory會利用 AspectJ 的類來解析 Aspect,並轉換為 spring-aop 需要的Advisor

    @Test
    public void test01() {
        
        AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
        // 設置被代理對象
        proxyFactory.setTarget(new UserService());
        
        // 添加Aspect
        proxyFactory.addAspect(UserServiceAspect.class);
        
        // 提前過濾不符合Poincut的類,這樣在執行 Advice chain 的時候就不要再過濾
        proxyFactory.setPreFiltered(true);
        
        UserService userController = (UserService)proxyFactory.getProxy();
        
        userController.save();
        userController.delete();
        userController.update();
        userController.find();
    }

關於 spring-aop 的組件、架構、使用等內容,就介紹到這里,第二篇博客再分析具體源碼。

感謝閱讀。以上內容如有錯誤,歡迎指正。

2021-09-27更改

相關源碼請移步: spring-aop

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/13671149.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM