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

子月生發表於2020-09-15

簡介

前面已經講完 spring-bean( 詳見Spring ),這篇部落格開始攻克 Spring 的另一個重要模組--spring-aop。

spring-aop 可以實現動態代理(底層是使用 JDK 動態代理或 cglib 來生成代理類),在專案中,一般被用來實現日誌、許可權、事務等的統一管理。

我將通過兩篇部落格來詳細介紹 spring-aop 的使用、原始碼等。這是第一篇部落格,主要介紹 spring-aop 的元件、架構、使用等。

專案環境

maven:3.6.3

作業系統:win10

JDK:8u231

Spring:5.2.6.RELEASE

幾個重要的元件

說到 spring-aop,我們經常會使用到PointcutJoinpointAdviceAspect等等基礎元件,它們都是抽象出來的“標準”,有的來自 aopalliance,有的來自 AspectJ,也有的是 spring-aop 原創。

想要學好 spring-aop,必須理解好這幾個基礎元件。但是,理解它們非常難,一個原因是網上能講清楚的不多,第二個原因是這些元件本身抽象得不夠直觀(spring 官網承認了這一點)。

AOP聯盟的元件--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

完整的 aopalliance 包,除了 aop 和 intercept,還包括了 instrument 和 reflect,後面這兩個部分 spring-aop 沒有引入,這裡就不說了。

使用 UML 表示以上類的關係,如下。可以看到,這主要包含兩個部分:JoinpointAdvice

AopAopallianceUML
  1. Joinpoint一個事件,包括呼叫某個方法(構造方法或成員方法)、操作某個成員屬性等

例如,我呼叫了user.study() 方法,這個事件本身就屬於一個JoinpointJoinpoint是一個“動態”的概念,通過它可以獲取這個事件的靜態資訊,例如當前事件對應的AccessibleObjectAccessibleObjectFieldMethodConstructor等的超類)。spring-aop 主要使用到Joinpoint的子介面MethodInvocation

  1. AdviceJoinpoint執行的某些操作

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

  1. Joinpoint 和 Advice 的關係

JoinpointAdvice操作的物件,一個Advice可以操作多個Joinpoint,一個Joinpoint也可以被多個Advice操作。在 spring-aop 裡,Joinpoint物件會持有一條Advice鏈,呼叫Joinpoint.proceed()將逐一執行其中的Advice(需要判斷是否執行),執行完AdviceAdvice鏈,將最終執行被代理物件的方法

AspectJ 的元件--Pointcut、Aspect

AspectJ 是一個非常非常強大的 AOP 工具,可以實現編譯期織入、編譯後織入和類載入時織入,並且提供了一套 AspectJ 語法(spring-aop 支援這套語法,但要額外引入 aspectjweaver 包)。spring-aop 使用到了 AspectJ 的兩個元件,PointcutAspect

其中,Pointcut可以看成一個過濾器,它可以用來判斷當前Advice是否攔截指定Joinpoint,如下圖所示。注意,不同的Advice也可以共用一個Pointcut

Pointcut_01

Aspect這個沒什麼特別,就是一組Pointcut+Advice的集合。下面這段程式碼中,有兩個Advice,分別為printRequestprintResponse,它們共享同一個Pointcut,而這個類裡的Pointcut+Advice可以算是一個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 的元件--Advisor

Advisor是 spring-aop 原創的一個元件,一個Advice加上它對應的過濾器,就組成了一個Advisor。在上面例子中,printRequestAdvice加上它的Pointcut,就是一個Advisor。而Aspect由多個Advisor組成。

注意,這裡的過濾器,可以是Pointcut,也可以是 spring-aop 自定義的ClassFilter

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-bean 瞭解( Spring原始碼系列(二)--bean元件的原始碼分析 )。

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

spring-aop 的代理工廠

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

ProxyFactoryUML

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 的元件、架構、使用等內容,就介紹到這裡,第二篇部落格再分析具體原始碼。

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

相關原始碼請移步: spring-aop

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

相關文章