Spring面向切面編程AOP詳情
1. 面向切面編程
- 定義:面向切面編程(AOP,Aspect Oriented Programming)是通過預(yù)編譯方式和運行期間動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)。
- 作用:利用AOP可以對業(yè)務(wù)邏輯的各個部分進行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
- 主要功能:日志記錄、性能統(tǒng)計、安全控制、事務(wù)處理、異常處理等。
- 總結(jié):面向切面編程是希望能夠?qū)⑼ㄓ眯枨蠊δ軓牟幌嚓P(guān)的類當(dāng)中分離出來,能夠使得很多類共享一個行為,一旦發(fā)生變化,不必修改很多類,而只是修改這個行為即可。
AOP通過提供另一種思考程序結(jié)構(gòu)的方式來補充了面向?qū)ο缶幊蹋∣OP)。OOP中模塊化的基本單元是類(class),而AOP中模塊化的基本單元是切面(aspect)??梢赃@么理解,OOP是解決了縱向的代碼復(fù)用問題,AOP是解決了橫向的代碼復(fù)用問題。
Spring的關(guān)鍵組件之一是AOP框架。雖然Spring IOC容器不依賴于AOP,意味著如果你不想使用AOP,則可以不使用AOP,但AOP補充了Spring IOC以提供一個非常強大的中間件解決方案。

2. AOP核心概念
- 切面(aspect):在AOP中,切面一般使用@Aspect注解來標(biāo)識。
- 連接點(Join Point):在Spring AOP,一個連接點總是代表一次方法的執(zhí)行。
- 增強(Advice):在連接點執(zhí)行的動作。
- 切入點(Pointcout):說明如何匹配到連接點。引
- 介(Introduction):為現(xiàn)有類型聲明額外的方法和屬性。
- 目標(biāo)對象(Target Object):由一個或者多個切面建議的對象,也被稱為“建議對象”,由于Spring AOP是通過動態(tài)代理來實現(xiàn)的,這個對象永遠是一個代理對象。
- AOP代理(AOP proxy):一個被AOP框架創(chuàng)建的對象,用于實現(xiàn)切面約定(增強方法的執(zhí)行等)。在Spring Framework中,一個AOP代理是一個JDK動態(tài)代理或者CGLIB代理。
- 織入(Weaving):連接切面和目標(biāo)對象或類型創(chuàng)建代理對象的過程。它能在編譯時(例如使用AspectJ編譯器)、加載時或者運行時完成。Spring AOP與其他的純Java AOP框架一樣是在運行時進行織入的。

Spring AOP包括以下類型的增強:
- 前置增強(Before advice):在連接點之前運行,但不能阻止到連接點的流程繼續(xù)執(zhí)行(除非拋出異常)
- 返回增強(After returning advice):在連接點正常完成后運行的增強(例如,方法返回沒有拋出異常)
- 異常增強(After thorwing advice):如果方法拋出異常退出需要執(zhí)行的增強
- 后置增強(After (finally) Advice):無論連接點是正常或者異常退出,都會執(zhí)行該增強
- 環(huán)繞增強(Around advice):圍繞連接點的增強,例如方法的調(diào)用。環(huán)繞增強能在方法的調(diào)用之前和調(diào)用之后自定義行為。它還可以選擇方法是繼續(xù)執(zhí)行或者去縮短方法的執(zhí)行通過返回自己的值或者拋出異常。
3. AOP的實現(xiàn)
AOP的兩種實現(xiàn)方式:靜態(tài)織入(以AspectJ為代表)和動態(tài)代理(Spring AOP實現(xiàn))
AspectJ是一個采用Java實現(xiàn)的AOP框架,它能夠?qū)Υa進行編譯(在編譯期進行),讓代碼具有AspectJ的AOP功能,當(dāng)然它也可支持動態(tài)代理的方式;
Spring AOP實現(xiàn):通過動態(tài)代理技術(shù)來實現(xiàn),Spring2.0集成了AspectJ,主要用于PointCut的解析和匹配,底層的技術(shù)還是使用的Spring1.x中的動態(tài)代理來實現(xiàn)。

Spring AOP采用了兩種混合的實現(xiàn)方式:JDK動態(tài)代理和CGLib動態(tài)代理。
JDK動態(tài)代理:Spring AOP的首選方法。每當(dāng)目標(biāo)對象實現(xiàn)一個接口時,就會使用JDK動態(tài)代理。目標(biāo)對象必須實現(xiàn)接口。
CGLIB:如果目標(biāo)對象沒有實現(xiàn)接口,則可以使用CGLIB代理。

4. Spring 對AOP支持
Spring可以使用兩種方式來實現(xiàn)AOP:基于注解式配置和基于XML配置
下面介紹基于注解配置的形式
4.1 支持@Aspect
如果是Spring Framework,需要使用aspectjweaver.jar包,然后創(chuàng)建我們自己的AppConfig,如下,并加上@EnableAspectJAutoProxy注解開啟AOP代理自動配置(Spring Boot默認是開啟的,則不需要增加配置),
如下:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}4.2 聲明一個切面
@Aspect //告訴Spring 這是一個切面
@Component //交給Spring容器管理
public class MyAspect {
}可以使用@Aspect來定義一個類作為切面,但是這樣,該類并不會自動被Spring加載,還是需要加上@Component注解
4.3 聲明一個切入點
一個切入點的生命包含兩個部分:一個包含名稱和任何參數(shù)的簽名和一個切入點的表達式,這個表達式確定了我們對哪些方法的執(zhí)行感興趣。
我們以攔截Controller層中的MyController中的test方法為例子,代碼如下:
@RestController
@RequestMapping("/my")
public class MyController {
@GetMapping("/test")
public void test() {
System.out.println("test 方法");
}
}下面定義一個名為controller的切入點,該切入點與上述的test方法相匹配,切入點需要用@Pointcut注解來標(biāo)注,如下:
//表達式
@Pointcut("execution (public * com.yc.springboot.controller.MyController.test())")
public void controller(){}; //簽名切入點表達式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) execution(方法修飾符(可選) 返回類型 類路徑 方法名 參數(shù) 異常模式(可選))
AspectJ描述符如下:
| AspectJ描述符 | 描述 |
| arg() | 限制連接點匹配參數(shù)為指定類型的執(zhí)行方法 |
| @args() | 限制連接點匹配參數(shù)由指定注解標(biāo)注的執(zhí)行方法 |
| execution() | 用于匹配是連接點的執(zhí)行方法 |
| this() | 限制連接點匹配的AOP代理的bean引用為指定類型的類 |
| target | 限制連接點匹配目標(biāo)對象為指定類型的類 |
| @target() | 限制連接點匹配特定的執(zhí)行對象,這些對象對應(yīng)的類要具有指定類型的注解 |
| within() | 限制連接點匹配指定的類型 |
| @within() | 限制連接點匹配指定注解所標(biāo)注的類型 |
| @annotationn | 限定匹配帶有指定注解的連接點 |
常用的主要是:execution()
AspectJ類型匹配的通配符:
| within() | 限制連接點匹配指定的類型 |
| @within() | 限制連接點匹配指定注解所標(biāo)注的類型 |
| @annotationn | 限定匹配帶有指定注解的連接點 |
常用的匹配規(guī)則:
| 表達式 | 內(nèi)容 |
| execution(public * *(..)) | 匹配所有public方法 |
| execution(* set*(..)) | 匹配所有方法名開頭為set的方法 |
| execution(* com.xyz.service.AccountService.*(..)) | 匹配AccountService下的所有方 |
| execution(* com.xyz.service.*.*(..)) | 匹配service包下的所有方法 |
| execution(* com.xyz.service..*.*(..)) | 匹配service包或其子包下的所有方法 |
| @annotation(org.springframework.transaction.annotation.Transactional) | 匹配所有打了@Transactional注解的方法 |
| bean(*Service) | 匹配命名后綴為Service的類的方法 |
4.4 聲明增強
增強與切點表達式相關(guān)聯(lián),并且在與切點匹配的方法之前、之后或者前后執(zhí)行。
在3當(dāng)中已經(jīng)對各類增強做了紹,這里就不詳細展開了,下面直接羅列了各種增強的聲明,用于攔截MyController中的各個方法
@Aspect //告訴Spring 這是一個切面
@Component //告訴Spring容器需要管理該對象
public class MyAspect {
//通過規(guī)則確定哪些方法是需要增強的
@Pointcut("execution (public * com.yc.springboot.controller.MyController.*(..))")
public void controller() {
}
//前置增強
@Before("controller()")
public void before(JoinPoint joinPoint) {
System.out.println("before advice");
}
//返回增強
@AfterReturning(
pointcut = "controller()",
returning = "retVal"
)
public void afterReturning(Object retVal) {
System.out.println("after returning advice, 返回結(jié)果 retVal:" + retVal);
}
//異常增強
@AfterThrowing(
pointcut = "controller()",
throwing = "ex"
)
public void afterThrowing(Exception ex) {
System.out.println("after throwing advice, 異常 ex:" + ex.getMessage());
}
//后置增強
@After("controller()")
public void after(JoinPoint joinPoint) {
System.out.println("after advice");
}
//環(huán)繞增強
@Around("controller()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("before advice");
//相當(dāng)于是before advice
Object reVal = null;
try {
reVal = joinPoint.proceed();
} catch (Exception e) {
//相當(dāng)于afterthrowing advice
System.out.println("afterthrowing advice");
}
//相當(dāng)于是after advice
System.out.println("after advice");
return reVal;
}
}需要注意的是:
- 在返回增強中,我們需要給@AfterReturing設(shè)置returning的值,且需要與方法的參數(shù)名一致,用于表示業(yè)務(wù)方法的返回值
- 在異常增強中,需要給@AfterThrowing設(shè)置throwing的值,且需要與方法的參數(shù)名一致,用于表示業(yè)務(wù)方法產(chǎn)生的異常在環(huán)繞增強中,參數(shù)為ProceedingJoinPoint類型,它是JoinPoint的子接口,我們需要在這個方法中手動調(diào)用其proceed方法來觸發(fā)業(yè)務(wù)方法
- 在所有的增強方法中都可以申明第一個參數(shù)為JoinPoint(注意的是,環(huán)繞增強是使用ProceedingJoinPoint來進行申明,它實現(xiàn)了JoinPoint接口)
- JoinPoint接口提供了幾個有用的方法 :
- getArgs():返回這個方法的參數(shù)
- getThis():返回這個代理對象
- getTarget():返回目標(biāo)對象(被代理的對象)
- getSignature():返回被增強方法的描述
- toString():打印被增強方法的有用描述
下面為Mycontroller測試類:
@RestController
@RequestMapping("/my")
public class MyController {
@GetMapping("/testBefore")
public void testBefore() {
System.out.println("testBefore 業(yè)務(wù)方法");
}
@GetMapping("/testAfterReturning")
public String testAfterReturning() {
System.out.println("testAfterReturning 業(yè)務(wù)方法");
return "我是一個返回值";
}
@GetMapping("/testAfterThrowing")
public void testAfterThrowing() {
System.out.println("testAfterThrowing 業(yè)務(wù)方法");
int a = 0;
int b = 1 / a;
}
@GetMapping("/testAfter")
public void testAfter() {
System.out.println("testAfter 業(yè)務(wù)方法");
}
@GetMapping("/around")
public void around() {
System.out.println("around 業(yè)務(wù)方法");
}
}5. 用AOP實現(xiàn)日志攔截
5.1 一般的實現(xiàn)
打印日志是AOP的一個常見應(yīng)用場景,我們可以對Controller層向外提供的接口做統(tǒng)一的日志攔截,用日志記錄請求參數(shù)、返回參數(shù)、請求時長以及異常信息,方便我們線上排查問題,下面是核心類LogAspect的實現(xiàn)
/**
* 日志的切面
*/
@Aspect
@Component
public class LogAspect {
@Resource
private IdWorker idWorker;
@Pointcut("execution (public * com.yc.core.controller.*.*(..))")
public void log(){}
/**
* 使用環(huán)繞增強實現(xiàn)日志打印
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("log()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
//獲得執(zhí)行方法的類和名稱
String className = joinPoint.getTarget().getClass().getName();
//獲得方法名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
//獲得參數(shù)
Object[] args = joinPoint.getArgs();
long requestId = idWorker.nextId();
//打印參數(shù)
LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",params:" + JSONObject.toJSONString(args));
long startTime = System.currentTimeMillis();
//執(zhí)行業(yè)務(wù)方法
Object result = null;
try {
result = joinPoint.proceed();
} catch (Exception e) {
LogHelper.writeErrLog(className, methodName, "requestId:" + requestId + ",異常啦:" + LogAspect.getStackTrace(e));
}
long endTime = System.currentTimeMillis();
//打印結(jié)果
LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",耗時:" + (endTime - startTime) + "ms,result:" + JSONObject.toJSONString(result));
//返回
return result;
}
/**
* 獲取異常的堆棧信息
* @param throwable
* @return
*/
public static String getStackTrace(Throwable throwable)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
try
{
throwable.printStackTrace(pw);
return sw.toString();
} finally
{
pw.close();
}
}
}- 在proceed()方法之前,相當(dāng)于前置增強,收集類名、方法名、參數(shù),記錄開始時間,生成requestId
- 在proceed()方法之后,相當(dāng)于后置增強,并能獲取到返回值,計算耗時
- 在前置增強時,生成requestId,用于串聯(lián)多條日志
- 使用try、catch包裹proceed()方法,在catch中記錄異常日志
- 提供了getStackTrace方法獲取異常的堆棧信息,便于排查報錯詳細情況
5.2 僅攔截需要的方法
但是上面的日志是針對所有controller層中的方法進行了日志攔截,如果我們有些方法不想進行日志輸出,比如文件上傳的接口、大量數(shù)據(jù)返回的接口,這個時候定義切入點的時候可以使用@annotation描述符來匹配加了特定注解的方法,步驟如下:
1. 先定義一個日志注解Log
/**
* 自定義日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}2.用@annotation定義切入點
@Pointcut("@annotation(com.yc.core.annotation.Log)")
public void logAnnotation(){}3.在想做日志輸出的方法上使用注解Log
@Log
@PostMapping(value = "testannotation")
public AOPTestVO testannotation(@RequestBody AOPTestDTO aopTestDTO) {
AOPTestVO aopTestVO = new AOPTestVO();
aopTestVO.setCode(1);
aopTestVO.setMsg("哈哈哈");
return aopTestVO;
}這樣,我們就可以自定義哪些方法需要日志輸出了
5.3 requestId傳遞
后來有同事提到,如果這是針對Controller層的攔截,但是Service層也有自定義的日志輸出,怎么在Service層獲取到上述的requestId呢?
其實就是我們攔截之后,是否可以針對方法的參數(shù)進行修改呢?其實注意是看
result = joinPoint.proceed();
我們發(fā)現(xiàn)ProceedingJoinPoint還有另外一個帶有參數(shù)的proceed方法,定義如下:
public Object proceed(Object[] args) throws Throwable;
我們可以利用這個方法,在環(huán)繞增強中去增加requestId,這樣后面的增強方法或業(yè)務(wù)方法中就能獲取到這個requestId了。
首先,我們先定義一個基類AOPBaseDTO,只有一個屬性requestId
@Data
@ApiModel("aop參數(shù)基類")
public class AOPBaseDTO {
@ApiModelProperty(value = "請求id", hidden = true)
private long requestId;
}然后我們讓Controller層接口的參數(shù)AOPTestDTO繼承上述AOPBaseDTO,如下:
@Data
@ApiModel("aop測試類")
public class AOPTestDTO extends AOPBaseDTO{
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "年齡")
private int age;
}最后在環(huán)繞的增強中添加上requestId,如下:
/**
* 使用環(huán)繞增強實現(xiàn)日志打印
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("logAnnotation()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
//獲得執(zhí)行方法的類和名稱
String className = joinPoint.getTarget().getClass().getName();
//獲得方法名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
//獲得參數(shù)
Object[] args = joinPoint.getArgs();
long requestId = idWorker.nextId();
for(int i = 0; i < args.length; i++) {
if (args[i] instanceof AOPBaseDTO) {
//增加requestId
((AOPBaseDTO) args[i]).setRequestId(requestId);
}
}
//打印參數(shù)
LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",params:" + JSONObject.toJSONString(args));
long startTime = System.currentTimeMillis();
//執(zhí)行業(yè)務(wù)方法
Object result = null;
try {
result = joinPoint.proceed(args);
} catch (Exception e) {
LogHelper.writeErrLog(className, methodName, "requestId:" + requestId + ",異常啦:" + LogAspect.getStackTrace(e));
}
long endTime = System.currentTimeMillis();
//打印結(jié)果
LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",耗時:" + (endTime - startTime) + "ms,result:" + JSONObject.toJSONString(result));
//返回
return result;
}我們運行起代碼,訪問一下,下面是運行結(jié)果:

可以看到,我們的業(yè)務(wù)方法中已經(jīng)能獲取到requestId,如果Service層需要,可以通過傳遞AOPTestDTO,從中獲取。
5.4 關(guān)于增強執(zhí)行的順序
- 1. 針對不同類型的增強,順序固定的,比如before在after前面
- 2. 針對同一切面的相同類型的增強,根據(jù)定義先后順序依次執(zhí)行
- 3. 針對不同切面的相同增強,可以通過使我們的切面實現(xiàn)Ordered接口,重寫getOrder方法,返回值最小,優(yōu)先級越高。注意的是before和after的相反的,before的優(yōu)先級越高越早執(zhí)行,after的優(yōu)先級越高,越晚執(zhí)行
6. 思考
- 1. 代理對象是什么時候創(chuàng)建的?
- 2. 當(dāng)存在多個不同類型增強時,執(zhí)行順序是怎么保證的?
- 3. 真正的業(yè)務(wù)方法是什么時候調(diào)用的,怎么做到只調(diào)用一次?
到此這篇關(guān)于Spring面向切面編程AOP詳情的文章就介紹到這了,更多相關(guān)Spring面向切面編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決springboot+thymeleaf視圖映射報錯There?was?an?unexpected?erro
這篇文章主要介紹了解決springboot+thymeleaf視圖映射報錯There?was?an?unexpected?error?(type=Not?Found,?status=404)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
在SpringBoot中如何利用Redis實現(xiàn)互斥鎖
當(dāng)我們利用Redis存儲熱點數(shù)據(jù)時,突然就過期失效或者被刪除了,導(dǎo)致大量請求同時訪問數(shù)據(jù)庫,增加了數(shù)據(jù)庫的負載,為減輕數(shù)據(jù)庫的負載我們利用互斥鎖,本文重點介紹在SpringBoot中如何利用Redis實現(xiàn)互斥鎖,感興趣的朋友一起看看吧2023-09-09
Spring在@ConditionalOnProperty注解使用詳解
這篇文章主要介紹了Spring在@ConditionalOnProperty注解使用詳解,@ConditionalOnProperty注解是Spring Boot的條件注解,主要用法是根據(jù)配置文件中的屬性來控制某個配置類是否生效,或者控制某個Bean是否被創(chuàng)建,需要的朋友可以參考下2023-11-11
Java使用Arrays.sort()方法實現(xiàn)給對象排序
這篇文章主要介紹了Java使用Arrays.sort()方法實現(xiàn)給對象排序,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Logger.getLogger()與LogFactory.getLog()的區(qū)別詳解
LogFactory來自common-logging包。如果用LogFactory.getLog,你可以用任何實現(xiàn)了通用日志接口的日志記錄器替換log4j,而程序不受影響2013-09-09

