1 理解AOP
1.1 什么是AOP
AOP(Aspect Oriented Programming)红碑,面向切面思想蔑穴,是Spring的三大核心思想之一(兩外兩個:IOC-控制反轉(zhuǎn)、DI-依賴注入)。
那么AOP為何那么重要呢叙量?在我們的程序中,經(jīng)常存在一些系統(tǒng)性的需求,比如權限校驗扑馁、日志記錄效诅、統(tǒng)計等篡腌,這些代碼會散落穿插在各個業(yè)務邏輯中,非常冗余且不利于維護米死。例如下面這個示意圖:
有多少業(yè)務操作物喷,就要寫多少重復的校驗和日志記錄代碼卤材,這顯然是無法接受的。當然峦失,用面向?qū)ο蟮乃枷肷却裕覀兛梢园堰@些重復的代碼抽離出來,寫成公共方法尉辑,就是下面這樣:
這樣晕拆,代碼冗余和可維護性的問題得到了解決,但每個業(yè)務方法中依然要依次手動調(diào)用這些公共方法,也是略顯繁瑣实幕。有沒有更好的方式呢吝镣?有的,那就是AOP昆庇,AOP將權限校驗末贾、日志記錄等非業(yè)務代碼完全提取出來,與業(yè)務代碼分離整吆,并尋找節(jié)點切入業(yè)務代碼中:
1.2 AOP體系與概念
簡單地去理解拱撵,其實AOP要做三類事:
- 在哪里切入,也就是權限校驗等非業(yè)務操作在哪些業(yè)務代碼中執(zhí)行表蝙。
- 在什么時候切入拴测,是業(yè)務代碼執(zhí)行前還是執(zhí)行后。
- 切入后做什么事府蛇,比如做權限校驗集索、日志記錄等。
因此汇跨,AOP的體系可以梳理為下圖:
一些概念詳解:
-
Pointcut
:切點务荆,決定處理如權限校驗、日志記錄等在何處切入業(yè)務代碼中(即織入切面)穷遂。切點分為execution
方式和annotation
方式函匕。前者可以用路徑表達式指定哪些類織入切面,后者可以指定被哪些注解修飾的代碼織入切面蚪黑。 -
Advice
:處理盅惜,包括處理時機和處理內(nèi)容。處理內(nèi)容就是要做什么事忌穿,比如校驗權限和記錄日志抒寂。處理時機就是在什么時機執(zhí)行處理內(nèi)容,分為前置處理(即業(yè)務代碼執(zhí)行前)伴网、后置處理(業(yè)務代碼執(zhí)行后)等。 -
Aspect
:切面妆棒,即Pointcut
和Advice
澡腾。 -
Joint point
:連接點,是程序執(zhí)行的一個點糕珊。例如动分,一個方法的執(zhí)行或者一個異常的處理。在 Spring AOP 中红选,一個連接點總是代表一個方法執(zhí)行澜公。 -
Weaving
:織入,就是通過動態(tài)代理,在目標對象方法中執(zhí)行處理內(nèi)容的過程坟乾。
網(wǎng)絡上有張圖迹辐,我覺得非常傳神,貼在這里供大家觀詳:
2 AOP實例
實踐出真知甚侣,接下來我們就擼代碼來實現(xiàn)一下AOP明吩。完成項目已上傳至:
使用 AOP,首先需要引入 AOP 的依賴殷费。參數(shù)校驗:這么寫參數(shù)校驗(validator)就不會被勸退了~
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.1 第一個實例
接下來印荔,我們先看一個極簡的例子:所有的get
請求被調(diào)用前在控制臺輸出一句"get請求的advice觸發(fā)了"。
具體實現(xiàn)如下:
- 創(chuàng)建一個AOP切面類详羡,只要在類上加個
@Aspect
注解即可。@Aspect
注解用來描述一個切面類,定義切面類的時候需要打上這個注解副硅。@Component
注解將該類交給 Spring 來管理拯杠。在這個類里實現(xiàn)advice:
package com.mu.demo.advice;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAdvice {
// 定義一個切點:所有被GetMapping注解修飾的方法會織入advice
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
private void logAdvicePointcut() {}
// Before表示logAdvice將在目標方法執(zhí)行前執(zhí)行
@Before("logAdvicePointcut()")
public void logAdvice(){
// 這里只是一個示例,你可以寫任何處理邏輯
System.out.println("get請求的advice觸發(fā)了");
}
}
- 創(chuàng)建一個接口類主到,內(nèi)部創(chuàng)建一個get請求:
package com.mu.demo.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/aop")
public class AopController {
@GetMapping(value = "/getTest")
public JSONObject aopTest() {
return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}");
}
@PostMapping(value = "/postTest")
public JSONObject aopTest2(@RequestParam("id") String id) {
return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}");
}
}
項目啟動后茶行,請求http://localhost:8085/aop/getTest
接口:
請求http://localhost:8085/aop/postTest
接口,控制臺無輸出登钥,證明切點確實是只針對被GetMapping
修飾的方法畔师。
2.2 第二個實例
下面我們將問題復雜化一些,該例的場景是:
- 自定義一個注解
PermissionsAnnotation
- 創(chuàng)建一個切面類牧牢,切點設置為攔截所有標注
PermissionsAnnotation
的方法看锉,截取到接口的參數(shù),進行簡單的權限校驗 - 將
PermissionsAnnotation
標注在測試接口類的測試接口test
上
具體的實現(xiàn)步驟:
- 使用
@Target塔鳍、@Retention伯铣、@Documented
自定義一個注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation{
}
- 創(chuàng)建第一個AOP切面類,轮纫,只要在類上加個
@Aspect
注解即可腔寡。@Aspect
注解用來描述一個切面類,定義切面類的時候需要打上這個注解掌唾。@Component
注解將該類交給 Spring 來管理放前。在這個類里實現(xiàn)第一步權限校驗邏輯:
package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1)
public class PermissionFirstAdvice {
// 定義一個切面,括號內(nèi)寫入第1步中自定義注解的路徑
@Pointcut("@annotation(com.mu.demo.annotation.PermissionAnnotation)")
private void permissionCheck() {
}
@Around("permissionCheck()")
public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================第一個切面===================:" + System.currentTimeMillis());
//獲取請求參數(shù)糯彬,詳見接口類
Object[] objects = joinPoint.getArgs();
Long id = ((JSONObject) objects[0]).getLong("id");
String name = ((JSONObject) objects[0]).getString("name");
System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// id小于0則拋出非法id的異常
if (id < 0) {
return JSON.parseObject("{\"message\":\"illegal id\",\"code\":403}");
}
return joinPoint.proceed();
}
}
- 創(chuàng)建接口類凭语,并在目標方法上標注自定義注解
PermissionsAnnotation
:
package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/permission")
public class TestController {
@RequestMapping(value = "/check", method = RequestMethod.POST)
// 添加這個注解
@PermissionsAnnotation()
public JSONObject getGroupList(@RequestBody JSONObject request) {
return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}");
}
}
在這里,我們先進行一個測試撩扒。首先似扔,填好請求地址和header:
其次,構造正常的參數(shù):
可以拿到正常的響應結(jié)果:
然后,構造一個異常參數(shù)炒辉,再次請求:
響應結(jié)果顯示豪墅,切面類進行了判斷,并返回相應結(jié)果:
有人會問辆脸,如果我一個接口想設置多個切面類進行校驗怎么辦但校?這些切面的執(zhí)行順序如何管理?
很簡單啡氢,一個自定義的AOP
注解可以對應多個切面類状囱,這些切面類執(zhí)行順序由@Order
注解管理,該注解后的數(shù)字越小倘是,所在切面類越先執(zhí)行亭枷。
下面在實例中進行演示:
創(chuàng)建第二個AOP切面類,在這個類里實現(xiàn)第二步權限校驗:
package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(0)
public class PermissionSecondAdvice {
@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {
}
@Around("permissionCheck()")
public Object permissionCheckSecond(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================第二個切面===================:" + System.currentTimeMillis());
//獲取請求參數(shù)搀崭,詳見接口類
Object[] objects = joinPoint.getArgs();
Long id = ((JSONObject) objects[0]).getLong("id");
String name = ((JSONObject) objects[0]).getString("name");
System.out.println("id->>>>>>>>>>>>>>>>>>>>>>" + id);
System.out.println("name->>>>>>>>>>>>>>>>>>>>>>" + name);
// name不是管理員則拋出異常
if (!name.equals("admin")) {
return JSON.parseObject("{\"message\":\"not admin\",\"code\":403}");
}
return joinPoint.proceed();
}
}
重啟項目叨粘,繼續(xù)測試,構造兩個參數(shù)都異常的情況:
響應結(jié)果瘤睹,表面第二個切面類執(zhí)行順序更靠前:
3 AOP相關注解
上面的案例中升敲,用到了諸多注解,下面針對這些注解進行詳解轰传。
3.1 @Pointcut
@Pointcut
注解驴党,用來定義一個切面,即上文中所關注的某件事情的入口获茬,切入點定義了事件觸發(fā)時機港庄。
@Aspect
@Component
public class LogAspectHandler {
/**
* 定義一個切面,攔截 com.itcodai.course09.controller 包和子包下的所有方法
*/
@Pointcut("execution(* com.mutest.controller..*.*(..))")
public void pointCut() {}
}
@Pointcut 注解指定一個切面恕曲,定義需要攔截的東西鹏氧,這里介紹兩個常用的表達式:一個是使用 execution()
,另一個是使用 annotation()
佩谣。
execution表達式:
以 execution(* * com.mutest.controller..*.*(..)))
表達式為例:
- 第一個 * 號的位置:表示返回值類型把还,* 表示所有類型。
- 包名:表示需要攔截的包名茸俭,后面的兩個句點表示當前包和當前包的所有子包吊履,在本例中指 com.mutest.controller包、子包下所有類的方法瓣履。
- 第二個 * 號的位置:表示類名率翅,* 表示所有類练俐。
- (..):這個星號表示方法名袖迎, 表示所有的方法,后面括弧里面表示方法的參數(shù),兩個句點表示任何參數(shù)燕锥。
annotation() 表達式:
annotation()
方式是針對某個注解來定義切面辜贵,比如我們對具有 @PostMapping 注解的方法做切面,可以如下定義切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}
然后使用該切面的話归形,就會切入注解是 @PostMapping
的所有方法托慨。這種方式很適合處理 @GetMapping、@PostMapping暇榴、@DeleteMapping
不同注解有各種特定處理邏輯的場景厚棵。
還有就是如上面案例所示,針對自定義注解來定義切面蔼紧。
@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {}
3.2 @Around
@Around
注解用于修飾Around
增強處理婆硬,Around
增強處理非常強大,表現(xiàn)在:
-
@Around
可以自由選擇增強動作與目標方法的執(zhí)行順序奸例,也就是說可以在增強動作前后彬犯,甚至過程中執(zhí)行目標方法。這個特性的實現(xiàn)在于查吊,調(diào)用ProceedingJoinPoint
參數(shù)的procedd()
方法才會執(zhí)行目標方法谐区。 -
@Around
可以改變執(zhí)行目標方法的參數(shù)值,也可以改變執(zhí)行目標方法之后的返回值逻卖。
Around
增強處理有以下特點:
- 當定義一個
Around
增強處理方法時宋列,該方法的第一個形參必須是ProceedingJoinPoint
類型(至少一個形參)。在增強處理方法體內(nèi)箭阶,調(diào)用ProceedingJoinPoint
的proceed
方法才會執(zhí)行目標方法:這就是@Around
增強處理可以完全控制目標方法執(zhí)行時機虚茶、如何執(zhí)行的關鍵;如果程序沒有調(diào)用ProceedingJoinPoint
的proceed
方法仇参,則目標方法不會執(zhí)行嘹叫。 - 調(diào)用
ProceedingJoinPoint
的proceed
方法時,還可以傳入一個Object[ ]
對象诈乒,該數(shù)組中的值將被傳入目標方法作為實參——這就是Around
增強處理方法可以改變目標方法參數(shù)值的關鍵罩扇。這就是如果傳入的Object[ ]
數(shù)組長度與目標方法所需要的參數(shù)個數(shù)不相等,或者Object[ ]
數(shù)組元素與目標方法所需參數(shù)的類型不匹配怕磨,程序就會出現(xiàn)異常喂饥。
@Around
功能雖然強大,但通常需要在線程安全的環(huán)境下使用肠鲫。因此员帮,如果使用普通的Before
、AfterReturning
就能解決的問題导饲,就沒有必要使用Around
了捞高。如果需要目標方法執(zhí)行之前和之后共享某種狀態(tài)數(shù)據(jù)氯材,則應該考慮使用Around
。尤其是需要使用增強處理阻止目標的執(zhí)行硝岗,或需要改變目標方法的返回值時氢哮,則只能使用Around
增強處理了。
下面型檀,在前面例子上做一些改造冗尤,來觀察@Around
的特點。
自定義注解類不變胀溺。首先裂七,定義接口類:
package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/permission")
public class TestController {
@RequestMapping(value = "/check", method = RequestMethod.POST)
@PermissionsAnnotation()
public JSONObject getGroupList(@RequestBody JSONObject request) {
return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200,\"data\":" + request + "}");
}
}
唯一切面類(前面案例有兩個切面類,這里只需保留一個即可):
package com.example.demo;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1)
public class PermissionAdvice {
@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {
}
@Around("permissionCheck()")
public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================開始增強處理===================");
//獲取請求參數(shù)仓坞,詳見接口類
Object[] objects = joinPoint.getArgs();
Long id = ((JSONObject) objects[0]).getLong("id");
String name = ((JSONObject) objects[0]).getString("name");
System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// 修改入?yún)? JSONObject object = new JSONObject();
object.put("id", 8);
object.put("name", "lisi");
objects[0] = object;
// 將修改后的參數(shù)傳入
return joinPoint.proceed(objects);
}
}
同樣使用JMeter調(diào)用接口碍讯,傳入?yún)?shù):{"id":-5,"name":"admin"}
,響應結(jié)果表明:@Around
截取到了接口的入?yún)⒊短桑⑹菇涌诜祷亓饲忻骖愔械慕Y(jié)果捉兴。
3.3 @Before
@Before
注解指定的方法在切面切入目標方法之前執(zhí)行,可以做一些 Log
處理录语,也可以做一些信息的統(tǒng)計倍啥,比如獲取用戶的請求 URL
以及用戶的 IP
地址等等,這個在做個人站點的時候都能用得到澎埠,都是常用的方法虽缕。例如下面代碼:
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 在上面定義的切面方法之前執(zhí)行該方法
* @param joinPoint jointPoint
*/
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("====doBefore方法進入了====");
// 獲取簽名
Signature signature = joinPoint.getSignature();
// 獲取切入的包名
String declaringTypeName = signature.getDeclaringTypeName();
// 獲取即將執(zhí)行的方法名
String funcName = signature.getName();
log.info("即將執(zhí)行方法為: {},屬于{}包", funcName, declaringTypeName);
// 也可以用來記錄一些信息蒲稳,比如獲取請求的 URL 和 IP
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 獲取請求 URL
String url = request.getRequestURL().toString();
// 獲取請求 IP
String ip = request.getRemoteAddr();
log.info("用戶請求的url為:{}氮趋,ip地址為:{}", url, ip);
}
}
JointPoint
對象很有用,可以用它來獲取一個簽名江耀,利用簽名可以獲取請求的包名剩胁、方法名,包括參數(shù)(通過 joinPoint.getArgs()
獲认楣)等昵观。
搜索程序員麥冬公眾號,回復“888”舌稀,送你一份2020最新Java面試題手冊.pdf
3.4 @After
@After
注解和 @Before
注解相對應啊犬,指定的方法在切面切入目標方法之后執(zhí)行,也可以做一些完成某方法之后的 Log 處理壁查。
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 定義一個切面觉至,攔截 com.mutest.controller 包下的所有方法
*/
@Pointcut("execution(* com.mutest.controller..*.*(..))")
public void pointCut() {}
/**
* 在上面定義的切面方法之后執(zhí)行該方法
* @param joinPoint jointPoint
*/
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
log.info("==== doAfter 方法進入了====");
Signature signature = joinPoint.getSignature();
String method = signature.getName();
log.info("方法{}已經(jīng)執(zhí)行完", method);
}
}
到這里,我們來寫個 Controller 測試一下執(zhí)行結(jié)果睡腿,新建一個 AopController 如下:
@RestController
@RequestMapping("/aop")
public class AopController {
@GetMapping("/{name}")
public String testAop(@PathVariable String name) {
return "Hello " + name;
}
}
啟動項目语御,在瀏覽器中輸入:localhost:8080/aop/csdn领斥,觀察一下控制臺的輸出信息:
====doBefore 方法進入了====
即將執(zhí)行方法為: testAop,屬于com.itcodai.mutest.AopController包
用戶請求的 url 為:http://localhost:8080/aop/name沃暗,ip地址為:0:0:0:0:0:0:0:1
==== doAfter 方法進入了====
方法 testAop 已經(jīng)執(zhí)行完
從打印出來的 Log
中可以看出程序執(zhí)行的邏輯與順序,可以很直觀的掌握 @Before
和 @After
兩個注解的實際作用何恶。
搜索程序員麥冬公眾號孽锥,回復“888”,送你一份2020最新Java面試題手冊.pdf
3.5 @AfterReturning
@AfterReturning
注解和 @After
有些類似细层,區(qū)別在于 @AfterReturning
注解可以用來捕獲切入方法執(zhí)行完之后的返回值惜辑,對返回值進行業(yè)務邏輯上的增強處理,例如:
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 在上面定義的切面方法返回后執(zhí)行該方法疫赎,可以捕獲返回對象或者對返回對象進行增強
* @param joinPoint joinPoint
* @param result result
*/
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("方法{}執(zhí)行完畢盛撑,返回參數(shù)為:{}", classMethod, result);
// 實際項目中可以根據(jù)業(yè)務做具體的返回值增強
log.info("對返回參數(shù)進行業(yè)務上的增強:{}", result + "增強版");
}
}
需要注意的是,在 @AfterReturning
注解 中捧搞,屬性 returning
的值必須要和參數(shù)保持一致抵卫,否則會檢測不到。該方法中的第二個入?yún)⒕褪潜磺蟹椒ǖ姆祷刂堤テ玻?doAfterReturning
方法中可以對返回值進行增強介粘,可以根據(jù)業(yè)務需要做相應的封裝。我們重啟一下服務晚树,再測試一下:
方法 testAop 執(zhí)行完畢姻采,返回參數(shù)為:Hello CSDN
對返回參數(shù)進行業(yè)務上的增強:Hello CSDN 增強版
3.6 @AfterThrowing
當被切方法執(zhí)行過程中拋出異常時,會進入 @AfterThrowing
注解的方法中執(zhí)行爵憎,在該方法中可以做一些異常的處理邏輯慨亲。要注意的是 throwing
屬性的值必須要和參數(shù)一致,否則會報錯宝鼓。該方法中的第二個入?yún)⒓礊閽伋龅漠惓刑棵!?/p>
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 在上面定義的切面方法執(zhí)行拋異常時,執(zhí)行該方法
* @param joinPoint jointPoint
* @param ex ex
*/
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
Signature signature = joinPoint.getSignature();
String method = signature.getName();
// 處理異常的邏輯
log.info("執(zhí)行方法{}出錯愚铡,異常為:{}", method, ex);
}
}
最后
感謝大家看到這里铐望,如果本文有什么不足之處,歡迎多多指教茂附;如果你覺得對你有幫助正蛙,請給我點個贊。
也歡迎大家關注我的公眾號:程序員麥冬营曼,每天更新行業(yè)資訊乒验!