前言
前面我們學(xué)習(xí)了 SpringBoot 統(tǒng)一功能處理,這篇文章我將為大家分享 Spring 框架的第二大核心——AOP(第一大核心是 IOC)
1. 什么是 AOP
AOP(Aspect Oriented Programming)是一種編程范型,意為面向切面編程,什么是?向切面編程呢?切面就是指某?類特定問題,所以AOP也可以理解為面向特定?法編程,它通過預(yù)編譯和運(yùn)行期動態(tài)代理的方式實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)。AOP可以看作是OOP(面向?qū)ο缶幊蹋┑难永m(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,同時(shí)也是函數(shù)式編程的一種衍生范型。
AOP的目標(biāo)是實(shí)現(xiàn)對業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而降低業(yè)務(wù)邏輯各部分之間的耦合度,提高程序的可重用性,并提升開發(fā)效率。在AOP中,目標(biāo)類代表核心代碼和業(yè)務(wù)邏輯,而額外功能(也即AOP的功能)則包括日志處理、事務(wù)處理、異常處理、性能分析等。通過將目標(biāo)類與額外功能結(jié)合,可以生成代理類。
AOP的原理基于Java的動態(tài)代理機(jī)制。通過預(yù)編譯方式和運(yùn)行期動態(tài)代理,AOP能夠?qū)崿F(xiàn)程序功能的統(tǒng)一維護(hù),使得開發(fā)者能夠更加專注于業(yè)務(wù)邏輯的實(shí)現(xiàn),而無需過多關(guān)注其他非核心功能。
AOP 是面向某一特定問題的編程?那前面的統(tǒng)一功能處理什么呢?其實(shí)前面學(xué)習(xí)的 統(tǒng)一功能處理是 AOP 的具體實(shí)現(xiàn)和應(yīng)用。AOP是?種思想,攔截器是AOP思想的?種實(shí)現(xiàn)。Spring框架實(shí)現(xiàn)了這種思想,提供了攔截器技術(shù)的相關(guān)接口。
2. 什么是 Spring AOP
知道了什么是 AOP,那么什么是 Spring AOP 呢?AOP 是一種思想,而 Spring AOP、AspectJ、GGLIB等叫做 AOP 的實(shí)現(xiàn)。
那么我們前面學(xué)習(xí)的攔截器、統(tǒng)一數(shù)據(jù)返回格式、統(tǒng)一異常處理這些 AOP 的實(shí)現(xiàn)不夠嗎?其實(shí)是不夠的,攔截器作?的維度是URL(?次請求和響應(yīng)),@ControllerAdvice 應(yīng)?場景主要是全局異常處理(配合?定義異常效果更佳),數(shù)據(jù)綁定,數(shù)據(jù)預(yù)處理。AOP作?的維度更加細(xì)致(可以根據(jù)包、類、?法名、參數(shù)等進(jìn)?攔截),能夠?qū)崿F(xiàn)更加復(fù)雜的業(yè)務(wù)邏輯。
假設(shè)一個(gè)項(xiàng)目中開發(fā)了很多業(yè)務(wù)功能,但是呢?由于一些業(yè)務(wù)的執(zhí)行效率比較低,耗時(shí)較長,所以我們就需要對接口進(jìn)行優(yōu)化。首先需要做的就是找到耗時(shí)較長的業(yè)務(wù)方法,那么我們?nèi)绾沃烂總€(gè)業(yè)務(wù)方法執(zhí)行的時(shí)間呢?一個(gè)簡單的方法就是可以通過獲取到業(yè)務(wù)方法剛執(zhí)行時(shí)候的時(shí)間 start,再記錄這個(gè)業(yè)務(wù)方法剛結(jié)束時(shí)候的時(shí)間 end,用 end - statr,就得到了該業(yè)務(wù)方法的執(zhí)行時(shí)間,那么,是否意味著我們需要在每個(gè)方法中都加上這段代碼呢?
public void function1() {
long startTime = System.currentTimeMillis();
test();
long endTime = System.currentTimeMillis();
log.info("function1執(zhí)行耗時(shí)" + (endTime - startTime));
}
如果每個(gè)方法都加上這樣的邏輯的話,如果業(yè)務(wù)中的方法很多的話,那么這也是一個(gè)很大的工作量,所以這時(shí)候就可以用到我們的 AOP 了,AOP可以做到在不改動這些原始?法的基礎(chǔ)上,針對特定的?法進(jìn)行功能的增強(qiáng)。
3. Spring AOP 的使用
引入 AOP 依賴
AOP 屬于第三方庫,要想使用的話,就需要在 pom.xml 文件中引入對應(yīng)的 AOP 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
編寫 AOP 程序
首先我們需要在類上加上 @Aspect
注解,表明這個(gè)類是一個(gè) AOP 類:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.example.springbootbook2.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
//執(zhí)行原始方法
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info(pjp.getSignature() + "執(zhí)行耗時(shí):{}ms",(end - begin));
return result;
}
}
- @Aspect:標(biāo)識這是?個(gè)切面類
- @Around:環(huán)繞通知,在目標(biāo)?法的前后都會被執(zhí)?。后?的表達(dá)式表示對哪些方法進(jìn)行增強(qiáng)
- pjp.proceed():讓原始?法執(zhí)行
因?yàn)?@Around
注解的作用,這個(gè)代碼被分為了三個(gè)部分:
看看執(zhí)行效果:
可以看到:使用 AOP 可以在不更改原來業(yè)務(wù)代碼的基礎(chǔ)上,做出一些額外的工作。
AOP 面向切面編程的優(yōu)勢:
- 代碼無侵入:不修改原始的業(yè)務(wù)方法,就可以對原始的業(yè)務(wù)方法進(jìn)行功能的增強(qiáng)或是功能的改變
- 減少重復(fù)代碼
- 提高開發(fā)效率
- 維護(hù)方便
既然 AOP 面向切面編程有這么多優(yōu)勢,拿那么接下來我們來詳細(xì)的學(xué)習(xí)一下 Spring AOP。
4. Spring AOP 詳解
4.1 Spring AOP 的概念
Spring AOP 需要了解的概念主要有:
- 切點(diǎn)
- 連接點(diǎn)
- 通知
- 切面
4.1.1 切點(diǎn)
切點(diǎn)(PointCur)也成為切入點(diǎn),作用是提供一組規(guī)則,告訴程序哪些方法來進(jìn)行功能增強(qiáng)。也就類似前面攔截器中的配置攔截路徑。
也就是這個(gè):
execution(* com.example.springbootbook2.controller.*.*(..))
叫做切點(diǎn)表達(dá)式,這里后面為大家詳細(xì)介紹。
4.1.2 連接點(diǎn)
滿足切點(diǎn)表達(dá)式規(guī)則的方法,就是連接點(diǎn),也就是可以被 AOP 控制的方法,參考上面的例子也就是 com.example.springboot2.controller
包下的所有類中的所有方法都叫做連接點(diǎn)。在我們上面的例子中主要體現(xiàn)在 pjp 參數(shù)中:
通過這個(gè)參數(shù)的 proceed()
方法可以執(zhí)行目標(biāo)方法。
4.1.3 通知
通知就是具體要做的工作,指那些重復(fù)的邏輯,也就是共性功能(最終體現(xiàn)為一個(gè)方法):
在AOP面向切面編程中,我們把這部分重復(fù)的代碼邏輯抽取出來單獨(dú)定義,這部分代碼就是通知的內(nèi)容。
4.1.4 切面
切面(Aspect)就是 切點(diǎn)(PointCut) + 通知(Advice)。
通過切面就能夠描述當(dāng)前 AOP 程序需要針對于哪些?法,在什么時(shí)候執(zhí)?什么樣的操作。切面既包含了通知邏輯的定義,也包括了連接點(diǎn)的定義。
一個(gè)切面類可以存在多個(gè)切面。
4.2 通知類型
Spring AOP 中的通知類型有以下幾種:
- @Around:環(huán)繞通知,此注解標(biāo)注的通知方法在目標(biāo)方法前,都被執(zhí)行
- @Before:前置通知,此注解標(biāo)注的通知方法在目標(biāo)方法前被執(zhí)行
- @After:后置通過,此注解標(biāo)注的通知方法在目標(biāo)方法后被執(zhí)行,?論是否有異常都會執(zhí)行
- AfterReturning:返回后通知,此注解標(biāo)注的通知方法在目標(biāo)方法后被執(zhí)行,有異常不會被執(zhí)行
- @AfterThrowing:異常后通知,此注解標(biāo)注的通知方法發(fā)生異常后執(zhí)行
接下來我們通過一個(gè)例子來了解這幾種通知類型:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
@Before("execution(* com.example.springaop.controller.*.*(..))")
public void doBefore() {
log.info("執(zhí)行 Before 方法");
}
@After("execution(* com.example.springaop.controller.*.*(..))")
public void doAfter() {
log.info("執(zhí)行 After 方法");
}
@AfterReturning("execution(* com.example.springaop.controller.*.*(..))")
public void doAfterReturning() {
log.info("執(zhí)行 AfterReturning 方法");
}
@AfterThrowing("execution(* com.example.springaop.controller.*.*(..))")
public void doAfterThrowing() {
log.info("執(zhí)行 AfterThrowing 方法");
}
@Around("execution(* com.example.springaop.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("Around 方法開始執(zhí)行");
Object result = pjp.proceed();
log.info("Around 方法執(zhí)行后");
return result;
}
}
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping("/t1")
public String t1() {
log.info("目標(biāo)方法執(zhí)行");
return "hello";
}
}
這里少了一個(gè)日志就是 AfterThrowing
方法打印的日志,這是因?yàn)槲覀冞@個(gè)方法沒有出現(xiàn)錯(cuò)誤,所以我們重新定義一個(gè)內(nèi)部會剖出異常的方法:
@RequestMapping("/t2")
public Integer t2() {
log.info("目標(biāo)方法2執(zhí)行");
Integer result = 10/0;
return result;
}
可以看到這幾個(gè)通知類型的執(zhí)行順序:
目標(biāo)方法中不出現(xiàn)異常:Around通知類型的目標(biāo)方法執(zhí)行前的邏輯——>Before通知類型——>目標(biāo)方法——>AfterReturning通知類型——>After通知類型——>Around通知類型的目標(biāo)方法執(zhí)行之后的邏輯
目標(biāo)方法中出現(xiàn)異常:Around通知類型的目標(biāo)方法執(zhí)行前的邏輯——>Before通知類型——>目標(biāo)方法——>AfterThrowing通知類型——>After通知類型
注意:
- @Around 環(huán)繞通知需要調(diào)用
ProceedingJoinPoint.proceed()
來讓原始方法執(zhí)行,其他通過不需要考慮目標(biāo)方法的執(zhí)行 - @Around 環(huán)繞通知方法的返回值,必須指定為 Object,來接收原始方法的返回值,否則原始方法執(zhí)行完畢,是獲取不到返回值的
- ?個(gè)切面類可以有多個(gè)切點(diǎn)
4.3 切點(diǎn)
通過前面的例子,我們可以發(fā)現(xiàn),同樣的切點(diǎn)表達(dá)式寫了很多,那么是否有一種方法可以節(jié)省相同切點(diǎn)表達(dá)式的書寫呢?當(dāng)然是可以的,我們程序員可是以“懶”著稱的。
我們可以使用 @PointCut
注解將相同的切點(diǎn)表達(dá)式提取出來,需要用到時(shí)引用該切點(diǎn)表達(dá)式即可。
@Pointcut("切點(diǎn)表達(dá)式")
修飾限定詞 返回值 方法名(){}
public class AspectDemo {
@Pointcut("execution(* com.example.springaop.controller.*.*(..))")
private void pt(){}
@Before("pt()")
public void doBefore() {
log.info("執(zhí)行 Before 方法");
}
@After("pt()")
public void doAfter() {
log.info("執(zhí)行 After 方法");
}
@AfterReturning("pt()")
public void doAfterReturning() {
log.info("執(zhí)行 AfterReturning 方法");
}
@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("執(zhí)行 AfterThrowing 方法");
}
@Around("pt()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("Around 方法開始執(zhí)行");
Object result = pjp.proceed();
log.info("Around 方法執(zhí)行后");
return result;
}
}
我們這里 @PointCut
注解的方法的修飾限定詞是 private,該切點(diǎn)表達(dá)式只能在當(dāng)前類中使用,如果我們想要在當(dāng)前項(xiàng)目的其他切面類中使用這個(gè)提取出來的切點(diǎn)表達(dá)式的話,需要將方法的修飾限定詞改為 public,并且引用這個(gè)切點(diǎn)表達(dá)式的方法需要使用 全限定類名.方法名()。
@Pointcut("execution(* com.example.springaop.controller.*.*(..))")
public void pt(){}
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.example.springaop.aspect.AspectDemo.pt()")
public void deBefore() {
log.info("執(zhí)行 AspectDemo2 中的 Before 方法");
}
}
4.4 切面優(yōu)先級 @Order注解
當(dāng)存在多個(gè)切面的時(shí)候,并且這些切面類的多個(gè)切入點(diǎn)都匹配到了同一個(gè)目標(biāo)方法,當(dāng)目標(biāo)方法運(yùn)行的時(shí)候,這些切面類中的通知方法都會執(zhí)行,這些通知方法的執(zhí)行順序會遵守兩個(gè)原則:
- 前面通知類型的先后順序
- 切面類的類名排序
我們創(chuàng)建出三個(gè)切面類,來看多個(gè)切面類匹配到了同一個(gè)目標(biāo)方法之后的通知方法的執(zhí)行順序:
通過這個(gè)運(yùn)行結(jié)果可以知道,當(dāng)存在多個(gè)切面類時(shí),默認(rèn)按照切面類名字母排序:
- @Before 通知:字母排名靠前的先執(zhí)行
- @After 通知,字母排名靠前的后執(zhí)行
就可以形象的看成這個(gè)模型:
但是根據(jù)切面類的類名字母排序的話,很不方便,所以為了切面類執(zhí)行順序更好的控制,就出現(xiàn)了一個(gè)注解 @Order()
用來控制多個(gè)切面類的執(zhí)行順序。
可以看到,通過使用 @Order
注解可以控制多個(gè)切面類的執(zhí)行順序。
4.5 切點(diǎn)表達(dá)式
前面我們使用切點(diǎn)表達(dá)式來描述切點(diǎn),接下來我們來詳細(xì)介紹一下切點(diǎn)表達(dá)式的語法。
切點(diǎn)表達(dá)式常見的有兩種表達(dá)方式:
- execution(…):根據(jù)方法的簽名來匹配
- @annotation(…):根據(jù)注解匹配
4.5.1 execution 切點(diǎn)表達(dá)式
execution() 是最常用的切點(diǎn)表達(dá)式,用來匹配方法,它的語法為:
execution(<訪問修飾符> <返回類型> <包名.類名.方法(方法參數(shù))> <異常>)
其中訪問修飾符和異??梢允÷浴?/p>
切點(diǎn)表達(dá)式支持通配符表達(dá):
- *:匹配任意字符,只匹配一個(gè)元素(返回類型,包,類名,方法或者方法參數(shù))
- 包名使用 * 表示任意包(一層包使用一個(gè)*)
- 類名使用 * 表示任意類
- 返回值使用 * 表示任意返回值類型
- ?法名使用 * 表示任意?法
- 參數(shù)使用 * 表示?個(gè)任意類型的參數(shù)
- … :匹配多個(gè)連續(xù)的任意符號,可以通配任意層級的包,或任意類型,任意個(gè)數(shù)的參數(shù)
- 使用 … 配置包名,表示此包以及此包下的所有子包
- 可以使用 … 配置參數(shù),任意個(gè)任意類型的參數(shù)
示例:
TestController 下的 public修飾,返回類型為 String 方法名為 t1,?參?法
execution(public String com.example.demo.controller.TestController.t1())
省略訪問修飾符:
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回類型:
execution(* com.example.demo.controller.TestController.t1())
匹配 TestController 下的所有無參方法:
execution(* com.example.demo.controller.TestController.*())
匹配 TestController 下的所有方法:
execution(* com.example.demo.controller.TestController.*(..))
匹配 controller 包下所有的類的所有方法:
execution(* com.example.demo.controller.*.*(..))
匹配所有包下?的 TestController:
execution(* com..TestController.*(..))
匹配 com.example.demo 包下,子孫包下的所有類的所有?法:
execution(* com.example.demo..*(..))
4.5.2 @annotation
execution 表達(dá)式適用于更符合規(guī)則的,如果我們要匹配更多無規(guī)則的方法的話,使用 execution 表達(dá)式就會很吃力。
假設(shè)我需要捕獲到 t2 和 t3 方法,使用 execution 表達(dá)式該如何寫呢?因?yàn)檫@兩個(gè)方法的返回值不同,使用 execution 表達(dá)式無法同時(shí)表示出這兩個(gè)方法。所以這時(shí)候就需要使用 @annotation
注解來捕獲到更多無規(guī)則的方法。
4.5.2.1 自定義注解
如何使用 @annotaion
呢?首先我們需要自定義出注解。
在新建 Java class 的時(shí)候選擇 Annotation:
然后里面的內(nèi)容我們就實(shí)現(xiàn)一個(gè)最簡單的注解,參考 @Component
注解來構(gòu)造一個(gè)自定義的注解:
@Target 標(biāo)識了 Annotation 所修飾的對象范圍,即該注解可以用在什么地方:
- ElementType.TYPE:?于描述類、接?(包括注解類型)或enum聲明
- ElementType.METHOD:描述方法
- ElementType.PARAMETER:描述參數(shù)
- ElementType.TYPE_USE:可以標(biāo)注任意類型
@Retention 指Annotation 被保留的時(shí)間長短,標(biāo)明注解的?命周期:
- RetentionPolicy.SOURCE:表示注解僅存在于源代碼中,編譯成字節(jié)碼后會被丟棄。這意味著在運(yùn)行時(shí)無法獲取到該注解的信息,只能在編譯時(shí)使?。比如 @SuppressWarnings ,以及l(fā)ombok提供的注解 @Data ,@Slf4j
- RetentionPolicy.CLASS:編譯時(shí)注解。表示注解存在于源代碼和字節(jié)碼中,但在運(yùn)行時(shí)會被丟棄。這意味著在編譯時(shí)和字節(jié)碼中可以通過反射獲取到該注解的信息,但在實(shí)際運(yùn)行時(shí)?法獲取。通常?于?些框架和?具的注解
- RetentionPolicy.RUNTIME:運(yùn)行時(shí)注解。表示注解存在于源代碼,字節(jié)碼和運(yùn)行時(shí)中。這意味著在編譯時(shí),字節(jié)碼中和實(shí)際運(yùn)行時(shí)都可以通過反射獲取到該注解的信息。通常?于?些需要在運(yùn)行時(shí)處理的注解,如Spring的 @Controller @ResponseBody
我們一般就使用 RUNTIME 來指明注解的存在時(shí)間。
package com.example.springaop.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
4.5.2.2 切面類
使用 @annotation 切點(diǎn)表達(dá)式定義切點(diǎn),只對 @MyAspect ?效:
@annotation 中需要填入我們自定義注解的全限定名。文章來源:http://www.zghlxwxcb.cn/news/detail-800955.html
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
@Before("@annotation(com.example.springaop.aspect.MyAspect)")
public void before() {
log.info("執(zhí)行 MyAspectDemo 的 Before 方法");
}
@After("@annotation(com.example.springaop.aspect.MyAspect)")
public void after() {
log.info("執(zhí)行 MyAspect 的 After 方法");
}
}
4.5.2.3 添加自定義注解
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping("/t1")
public String t1() {
log.info("目標(biāo)方法執(zhí)行");
return "hello";
}
@MyAspect
@RequestMapping("/t2")
public String t2() {
log.info("我愛Java,我要成為Java高手");
return "hello world";
}
@MyAspect
@RequestMapping("/t3")
public Integer t3() {
log.info("目標(biāo)方法2執(zhí)行");
Integer result = 10/0;
return result;
}
}
可以看到通過使用 @annotation
可以更加靈活的捕獲到無規(guī)則的方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-800955.html
到了這里,關(guān)于【Spring】Spring AOP的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!