溫故知新——Spring AOP

牛初九發表於2020-08-27

Spring AOP 面向切面程式設計,相信大家都不陌生,它和Spring IOC是Spring賴以成名的兩個最基礎的功能。在我們們平時的工作中,使用IOC的場景比較多,像我們們平時使用的@Controller、@Service、@Repository、@Component、@Autowired等,這些都和IOC相關。但是,使用AOP的場景卻非常少,也就是在事務控制這裡使用到了AOP,隨著SpringBoot的流行,事務控制這塊也不用自己配置了,SpringBoot內部已經給我們們配置好了,我們只需要使用@Transactional這個註解就可以了。

Spring AOP作為Spring的基礎功能,大家在學習的時候肯定都學過,但是由於平時使用的比較少,漸漸的就遺忘了,今天我們就再來看看Spring AOP,全面的給大家講一下,我本人也忘記的差不多了,在面試的時候,人家問我Spring AOP怎麼使用,我回答:呵呵,忘得差不多了。對方也是微微一笑,回了一個呵呵。好了,我們們具體看看Spring AOP吧。

Spring AOP解決的問題

面向切面程式設計,通俗的講就是將你執行的方法橫切,在方法前、後或者丟擲異常時,執行你額外的程式碼。比如:你想要在執行所有的方法前,要驗證當前的使用者有沒有許可權執行這個方法。如果沒有AOP,你的做法是寫個驗證使用者許可權的方法,然後在所有的方法中,都去呼叫這個公共方法,如果有許可權再去執行後面的方法。這樣做是可以的,但是顯得比較囉嗦,而且硬編碼比較多,如果哪個小朋友忘了這個許可權驗證,那就麻煩了。

現在我們有了AOP,只需要幾個簡單的配置,就可以在所有的方法之前,去執行我們的驗證許可權的公共方法。

Sping AOP的核心概念

在AOP當中,核心的術語非常多,有8個,而且理解起來也是晦澀難懂,在這裡給大家羅列一下,大家如果感興趣可以去查閱一下其他的資料。

AOP的8個術語:切面(Aspect)、連線點(Join point)、通知(Advice)、切點(Pointcut)、引入(Introduction)、目標物件(Target object)、AOP代理(AOP proxy)、編織(Weaving)。

在這裡,我個人覺得Spring AOP總結成以下幾個概念就可以了。

  • 切面(Aspect):在Spring AOP的實際使用中,只是標識的這一段是AOP的配置,沒有其他的意義。
  • 切點(Pointcut):就是我們的方法中,哪些方法需要被代理,它需要一個表示式,凡是匹配成功的方法都會執行你定義的通知。
  • 通知(Advice):就是你要另外執行的方法,在前面的例子中,就是許可權校驗的方法。

好了,知道這幾個概念,我個人覺得在平時工作中已經足夠了。

Sping AOP的具體配置——註解

我們的例項採用SpringBoot專案搭建,首先我們要把Spring AOP的依賴新增到專案中,具體的maven配置如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

具體的版本我們跟隨SpringBoot的版本即可。既然它是個starter,我比較好奇它在配置檔案中有哪些配置,我們到application.properties中去看一下,

spring.aop.auto=true
spring.aop.proxy-target-class=true
  • spring.aop.auto:它的預設值是true,它的含義是:Add @EnableAspectJAutoProxy,也就是說,我們在配置類上不再需要標註@EnableAspectJAutoProxy了。這個算是Spring AOP中的一個知識點了,我們重點說一下。

我們在Spring中使用AOP,需要兩個條件,第一個,要在我們的專案中引入aspectjweaver.jar,這個,我們在引入spring-boot-starter-aop的時候已經自動引入了。第二個,就是我們我們的配置類中,標註@EnableAspectJAutoProxy 這個註解,如果你使用的是xml的方式,需要你在xml檔案中標明<aop:aspectj-autoproxy/>。這樣才能在我們的專案使用Spring-AOP。

  • spring.aop.proxy-target-class:AOP代理的實現,這個值預設也是true。它的含義是是否使用CGLIB代理。這也是AOP中的一個知識點。

Spring AOP的代理有兩種,一種是標準的JDK動態代理,它只能代理介面,也即是我們在使用的時候,必須寫一個介面和實現類,在實現類中寫自己的業務邏輯,然後通過介面實現AOP。另外一種是使用CGLIB 代理,它可以實現對類的代理,這樣我們就不用去寫介面了。

Spring AOP預設使用的是JDK動態代理,只能代理介面,而我們在開發的時候為了方便,希望可以直接代理類,這就需要引入CGLIB ,spring.aop.proxy-target-class預設是true,使用CGLIB,我們可以放心大膽的直接代理類了。

通過前面的步驟,我們已經可以在專案中使用Spring AOP代理了,下面我們先建立個Service,再寫個方法,如下:

@Service
public class MyService {

    public void gotorun() {
        System.out.println("我要去跑步!");
    }

}

我們寫了個“我要去跑步”的方法,然後再通過AOP,在方法執行之前,列印出“我穿上了跑鞋”。下面重點看一下這個AOP代理怎麼寫,

@Component
@Aspect
public class MyAspect {

    @Pointcut("execution(public * com.example.springaopdemo.service.*.*(..))")
    private void shoes() {}

    @Before("com.example.springaopdemo.aspect.MyAspect.shoes()")
    public void putonshoes() {
        System.out.println("我穿上跑步鞋。");
    }
}

首先,要建立一個切面,我們在類上使用@Aspect註解,標識著這個類是一個切面,而@Component註解是將這個類例項化,這樣這個切面才會起作用。如果只有@Aspect而沒有被Spring例項是不起作用的。當然Spring例項化的方法有很多,不一定就非要使用@Component。

再來看看切點的宣告,切點的作用是匹配哪些方法要使用這個代理,我們使用@Pointcut註解。@Pointcut註解中有個表示式就是匹配我們的方法用到,它的種類也有很多,這裡我們給大家列出幾個比較常用的execution表示式吧,

execution(public * *(..))      //匹配所有的public方法
execution(* set*(..))          //匹配所有以set開頭的方法
execution(* com.xyz.service.AccountService.*(..))     //匹配所有AccountService中的方法
execution(* com.xyz.service.*.*(..))    //匹配所有service包中的方法
execution(* com.xyz.service..*.*(..))   //匹配所有service包及其子包中的方法

示例中,我們匹配的是service包中的所有方法。

有沒有同學比較好奇@Pointcut下面的那個方法?這個方法到底有沒有用?方法中如果有其他的操作會不會執行?答案是:方法裡的內容不會被執行。那麼它有什麼用呢?它僅僅是給@Pointcut一個落腳的地方,僅此而已。但是,Spring對一個方法也是有要求的,這個方法的返回值型別必須是void。原文是這麼寫的:

the method serving as the pointcut signature must have a void return type

最後我們再來看看通知,通知的種類有5種,分別為:

  • @Before:前置通知,在方法之前執行;
  • @AfterReturning:返回通知,方法正常返回以後,執行通知方法;
  • @AfterThrowing:異常通知,方法丟擲異常後,執行通知方法;
  • @After:也是返回通知,不管方法是否正常結束,都會執行這個方法,類似於finally;
  • @Around:環繞通知,在方法執行前後,都會執行通知方法;

在示例中,使用的是@Before前置通知,我們最關心的是@Before裡的內容:

@Before("com.example.springaopdemo.aspect.MyAspect.shoes()")
public void putonshoes() {
    System.out.println("我穿上跑步鞋。");
}

@Before裡的內容是切點的方法,也就是我們定義的shoes()方法。那麼所有匹配了shoes()切點的方法,都會執行@Before這個註解的方法,也就是putonshoes()。

在@Before裡除了寫切點的方法,還可以直接寫切點表示式,例如:

@Before("execution(public * com.example.springaopdemo.service.*.*(..))")
public void putonshoes() {
    System.out.println("我穿上跑步鞋。");
}

如果我們使用這種表示式的寫法,就可以省去前面的@Pointcut了,這種方法還是比較推薦的。我們再寫個測試類執行一下,看看效果吧,

@SpringBootTest
class SpringAopDemoApplicationTests {
    @Autowired
    private MyService myService;

    @Test
    public void testAdvice() {
        myService.gotorun();
    }
}

執行結果如下:

我穿上跑步鞋。
我要去跑步!

沒有問題,在執行“我要去跑步”之前,成功的執行了“我穿上跑步鞋”的方法。

好了,今天先到這裡,下一篇我們看看如何使用xml的方式配置Spring AOP。

相關文章