Spring AOP - 註解方式使用介紹(長文詳解)

前言

以前的源碼解析章節,本人講解了Spring IOC 的核心部分的源碼。若是你熟悉Spring AOP的使用的話,在瞭解Spring IOC的核心源碼以後,學習Spring AOP 的源碼,應該能夠說是水到渠成,不會有什麼困難。java

可是直接開始講Spring AOP的源碼,本人又以爲有點突兀,因此便有了這一章。Spring AOP 的入門使用介紹:包括Spring AOP的一些概念性介紹和配置使用方法。web

這裏先貼一下思惟導圖。spring

Spring AOP.png

AOP 是什麼

AOP : 面向切面編程(Aspect Oriented Programming)

Aspect是一種新的模塊化機制,用來描述分散在對象、類或函數中的橫切關注點(crosscutting concern)。從關注點中分離出橫切關注點是面向切面的程序設計的核心概念。分離關注點使解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中再也不含有針對特定領域問題代碼的調用,業務邏輯同特定領域問題的關係經過切面來封裝、維護,這樣本來分散在整個應用程序中的變更就能夠很好地管理起來。express

最近在看李智慧的《大型網站技術架構》一書中,做者提到,開發低耦合系統是軟件設計的終極目標之一。AOP這種面向切面編程的的方式就體現了這樣的理念。將一些重複的、和業務主邏輯不相關的功能性代碼(日誌記錄、安全管理等)經過切面模塊化地抽離出來進行封裝,實現關注點分離、模塊解耦,使得整個系統更易於維護管理。編程

這樣分而治之的設計,讓我感受到了一種美感。安全

AOP 要實現的是在咱們原來寫的代碼的基礎上,進行必定的包裝,如在方法執行前、方法返回後、方法拋出異常後等地方進行必定的攔截處理或者叫加強處理。架構

AOP 的實現並非由於 Java 提供了什麼神奇的鉤子,能夠把方法的幾個生命週期告訴咱們,而是咱們要實現一個代理,實際運行的實例實際上是生成的代理類的實例併發

名詞概念

前面提到過,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的註解。也就是Spring AOP裏面的概念和術語,並非Spring獨有的,而是和AOP相關的。app

概念能夠草草看過,在看了以後的章節以後再回來看會對概念理解的更深。框架

術語 概念
Aspect 切面是PointcutAdvice的集合,通常單獨做爲一個類。PointcutAdvice共同定義了關於切面的所有內容,它是何時,在什麼時候和何處完成功能。
Joinpoint 這表示你的應用程序中能夠插入AOP方面的一點。也能夠說,這是應用程序中使用Spring AOP框架採起操做的實際位置。
Advice 這是在方法執行以前或以後採起的實際操做。 這是在Spring AOP框架的程序執行期間調用的實際代碼片斷。
Pointcut 這是一組一個或多個切入點,在切點應該執行Advice。 您可使用表達式或模式指定切入點,後面示例會提到。
Introduction 引用容許咱們向現有的類添加新的方法或者屬性
Weaving 建立一個被加強對象的過程。這能夠在編譯時完成(例如使用AspectJ編譯器),也能夠在運行時完成。Spring和其餘純Java AOP框架同樣,在運行時完成織入。
PS:在整理概念的時候有個疑問,爲何網上這麼多中文文章把advice 翻譯成「通知」呢???概念上說得通嗎???我更願意翻譯成「加強」(併發中文網ifeve.com 也是翻譯成加強)

還有一些註解,表示Advice的類型,或者說加強的時機,看過以後的示例以後會更加的清楚。

術語 概念
Before 在方法被調用以前執行加強
After 在方法被調用以後執行加強
After-returning 在方法成功執行以後執行加強
After-throwing 在方法拋出指定異常後執行加強
Around 在方法調用的先後執行自定義的加強行爲(最靈活的方式)

使用方式

Spring 2.0 以後,Spring AOP有了兩種配置方式。

  1. schema-based:Spring 2.0 之後使用 XML 的方式來配置,使用 命名空間 <aop />
  2. @AspectJ 配置:Spring 2.0 之後提供的註解方式。這裏雖然叫作 @AspectJ,可是這個和 AspectJ 其實沒啥關係。
PS:我的比較鍾情於@AspectJ 這種方式,使用下來是最方面的。也多是由於我以爲XML方式配置的Spring Bean很不簡潔、寫起來很差看吧,因此有點排斥吧。23333~

本文主要針對註解方式講解,而且給出對應的DEMO;以後的源碼解析也會以註解的這種方式爲範例講解Spring AOP的源碼(整個源碼解析看完,會對其餘方式舉一反三,由於原理都是同樣的)

若是對其餘配置方式感興趣的同窗能夠google其餘的學習資料。


來一條分割線,正式開始

1. 開啓@AspectJ 註解配置方式

開啓@AspectJ的註解配置方式,有兩種方式

  1. 在XML中配置:

    <aop:aspectj-autoproxy/>
  2. 使用@EnableAspectJAutoProxy註解

    @Configuration
    @EnableAspectJAutoProxy
    public class Config {
    
    }

開啓了上述配置以後,全部在容器中@AspectJ註解的 bean 都會被 Spring 當作是 AOP 配置類,稱爲一個 Aspect。

NOTE:這裏有個要注意的地方,@AspectJ 註解只能做用於Spring Bean 上面,因此你用 @Aspect 修飾的類要麼是用 @Component註解修飾,要麼是在 XML中配置過的。

好比下面的寫法,

// 有效的AOP配置類
@Aspect
@Component
public class MyAspect {
     //....   
}

// 若是沒有在XML配置過,那這個就是無效的AOP配置類
@Aspect
public class MyAspect {
     //....   
}

2. 配置 Pointcut (加強的切入點)

Pointcut 在大部分地方被翻譯成切點,用於定義哪些方法須要被加強或者說須要被攔截。

在Spring 中,咱們能夠認爲 Pointcut 是用來匹配Spring 容器中全部知足指定條件的bean的方法。

好比下面的寫法,

// 指定的方法
    @Pointcut("execution(* testExecution(..))")
    public void anyTestMethod() {}

下面完整列舉一下 Pointcut 的匹配方式:

  1. execution:匹配方法簽名

    這個最簡單的方式就是上面的例子,"execution(* testExecution(..))"表示的是匹配名爲testExecution的方法,*表明任意返回值,(..)表示零個或多個任意參數。

  2. within:指定所在類或所在包下面的方法(Spring AOP 獨有)

    // service 層
        // ".." 表明包及其子包
        @Pointcut("within(ric.study.demo.aop.svc..*)")
        public void inSvcLayer() {}
  3. @annotation:方法上具備特定的註解

    // 指定註解
        @Pointcut("@annotation(ric.study.demo.aop.HaveAop)")
        public void withAnnotation() {}
  4. bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 獨有)

    // controller 層
        @Pointcut("bean(testController)")
        public void inControllerLayer() {}
上述是平常使用中常見的幾種配置方式

有更細的匹配需求的,能夠參考這篇文章:https://www.baeldung.com/spri...

關於 Pointcut 的配置,Spring 官方有這麼一段建議:

When working with enterprise applications, you often want to refer to modules of the application and particular sets of operations from within several aspects. We recommend defining a "SystemArchitecture" aspect that captures common pointcut expressions for this purpose. A typical such aspect would look as follows:

意思就是,若是你是在開發企業級應用,Spring 建議你使用 SystemArchitecture這種切面配置方式,即將一些公共的PointCut 配置所有寫在這個一個類裏面維護。官網文檔給的例子像下面這樣(它文中使用 XML 配置的,因此沒加@Component註解)

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  /**
   * A join point is in the web layer if the method is defined
   * in a type in the com.xyz.someapp.web package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  /**
   * A join point is in the service layer if the method is defined
   * in a type in the com.xyz.someapp.service package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  /**
   * A join point is in the data access layer if the method is defined
   * in a type in the com.xyz.someapp.dao package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  /**
   * A business service is the execution of any method defined on a service
   * interface. This definition assumes that interfaces are placed in the
   * "service" package, and that implementation types are in sub-packages.
   * 
   * If you group service interfaces by functional area (for example, 
   * in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
   * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
   * could be used instead.
   */
  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}
  
  /**
   * A data access operation is the execution of any method defined on a 
   * dao interface. This definition assumes that interfaces are placed in the
   * "dao" package, and that implementation types are in sub-packages.
   */
  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}

}

上面這個 SystemArchitecture 很好理解,該 Aspect 定義了一堆的 Pointcut,隨後在任何須要 Pointcut 的地方均可以直接引用。

配置切點,表明着咱們想讓程序攔截哪一些方法,但程序須要怎麼對攔截的方法進行加強,就是後面要介紹的配置 Advice。

3. 配置Advice

注意,實際開發過程中,Aspect 類應該遵照單一職責原則,不要把全部的Advice配置所有寫在一個Aspect類裏面。

這裏是爲了演示方便,因此寫在了一塊兒。

先直接上示例代碼,裏面包含了Advice 的幾種配置方式(上文名詞概念小節中有提到)。

/**
 * 注:實際開發過程中,Advice應遵循單一職責,不該混在一塊兒
 *
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Aspect
@Component
public class GlobalAopAdvice {

    @Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // 實際使用過程中 能夠像這樣把Advice 和 Pointcut 合在一塊兒,直接在Advice上面定義切入點
    @Before("execution(* ric.study.demo.dao.*.*(..))")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // 在方法
    @AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // returnVal 就是相應方法的返回值
    @AfterReturning(
        pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()",
        returning="returnVal")
    public void doAccessCheck(Object returnVal) {
        //  ... 實現代碼
    }

    // 異常返回的時候
    @AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ... 實現代碼
    }

    // 注意理解它和 @AfterReturning 之間的區別,這裏會攔截正常返回和異常的狀況
    @After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 一般就像 finally 塊同樣使用,用來釋放資源。
        // 不管正常返回仍是異常退出,都會被攔截到
    }

    // 這種最靈活,既能作 @Before 的事情,也能夠作 @AfterReturning 的事情
    @Around("ric.study.demo.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
           //  target 方法執行前... 實現代碼
        Object retVal = pjp.proceed();
        //  target 方法執行後... 實現代碼
        return retVal;
    }
}

在某些場景下,咱們想在@Before的時候,去獲取方法的入參,好比進行一些日誌的記錄,咱們能夠經過 org.aspectj.lang.JoinPoint 來實現。上文中的ProceedingJoinPoint就是其子類。

@Before("...")
public void logArgs(JoinPoint joinPoint) {
    System.out.println("方法執行前,打印入參:" + Arrays.toString(joinPoint.getArgs()));
}

再舉個與之對應的,方法返參打印:

@AfterReturning( pointcut="...", returning="returnVal")
public void logReturnVal(Object returnVal) {
    System.out.println("方法執行後,打印返參:" + returnVal));
}

快速Demo

介紹完上述的配置過程以後,咱們用一個快速的Demo來實際演示一遍。這裏把順序變一下;

1. 編寫 目標類

package ric.study.demo.aop.svc;

public interface TestSvc {

    void process();
}

@Service("testSvc")
public class TestSvcImpl implements TestSvc {
    @Override
    public void process() {
        System.out.println("test svc is working");
    }
}

public interface DateSvc {

    void printDate(Date date);
}

@Service("dateSvc")
public class DateSvcImpl implements DateSvc {

    @Override
    public void printDate(Date date) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
    }
}

2. 配置 Pointcut

@Aspect
@Component
public class PointCutConfig {
    @Pointcut("within(ric.study.demo.aop.svc..*)")
    public void inSvcLayer() {}   
}

3. 配置Advice

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/29
 */
@Component
@Aspect
public class ServiceLogAspect {

    // 攔截,打印日誌,而且經過JoinPoint 獲取方法參數
    @Before("ric.study.demo.aop.PointCutConfig.inSvcLayer()")
    public void logBeforeSvc(JoinPoint joinPoint) {
        System.out.println("在service 方法執行前 打印第 1 第二天志");
        System.out.println("攔截的service 方法的方法簽名: " + joinPoint.getSignature());
        System.out.println("攔截的service 方法的方法入參: " + Arrays.toString(joinPoint.getArgs()));
    }

    // 這裏是Advice和Pointcut 合在一塊兒配置的方式
    @Before("within(ric.study.demo.aop.svc..*)")
    public void logBeforeSvc2() {
        System.out.println("在service的方法執行前 打印第 2 第二天志");
    }
}

4. 開啓@AspectJ 註解配置方式,並啓動

這裏爲了圖方便,把配置類和啓動類寫在了一塊兒,

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("ric.study.demo.aop")
public class Boostrap {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Boostrap.class);
        TestSvc svc = (TestSvc) context.getBean("testSvc");
        svc.process();
        System.out.println("==================");
        DateSvc dateSvc = (DateSvc) context.getBean("dateSvc");
        dateSvc.printDate(new Date());
    }
}

5. 輸出

在service 方法執行前 打印第 1 第二天志
攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.TestSvcImpl.process()
攔截的service 方法的方法入參: []
在service的方法執行前 打印第 2 第二天志
test svc is working
==================
在service 方法執行前 打印第 1 第二天志
攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.DateSvcImpl.printDate(Date)
攔截的service 方法的方法入參: [Mon Nov 04 18:11:34 CST 2019]
在service的方法執行前 打印第 2 第二天志
2019-11-04 18:11:34

JDK 動態代理和 Cglib

前面有提到過,Spring AOP在目標類有實現接口的時候,會使用JDK 動態代理來生成代理類,咱們結合上面的DEMO看看,

image.png

若是咱們想不論是否有實現接口,都是強制使用Cglib的方式來實現怎麼辦?

Spring 提供給了咱們對應的配置方式,也就是proxy-target-class.

註解方式:
//@EnableAspectJAutoProxy(proxyTargetClass = true) // 這樣子就是默認使用CGLIB
XML方式:
<aop:config proxy-target-class="true">

改了以後,

image.png

小結

本文詳細介紹了Spring AOP的起源、名詞概念以及基於註解的使用方式。

本文按照做者的寫做習慣,是源碼解析章節的前置學習章節。在下一章中,咱們會以註解方式爲入口,介紹Spring AOP 的源碼設計,解讀相關核心源碼(整個源碼解析看完,會對其餘方式舉一反三,由於原理都是同樣的)。

感興趣的能夠翻到【前言】部分,再看一下思惟導圖。

若是本文有幫助到你,但願能點個贊,這是對個人最大動力。

相關文章
相關標籤/搜索