javascript
SpringBoot-切面AOP实现统一逻辑处理
AOP概述
AOP(Aspect Oriented Programming),面向切面思想,與IOC(控制反轉)、DI(依賴注入)組成Spring的三大核心思想。既然是核心,那肯定是重要的。那么他為什么重要,以及在實際應用場景中我們可以用它來做什么呢?
不知道大家在開發過程中有沒有遇到過這樣的系統性需求:統計,權限檢驗,日志記錄等等。可能很多人想到的就是在每一個調用方法的業務邏輯中都寫一遍檢驗或者記錄日志或者統計,例如我們需要在調用方法前進行一下權限的校驗,在方法結束以后記錄一下操作的日志,示意圖如下:
這樣做也的確是可以滿足我們的需求,但是在實際的維護過程中非常不利,有多少業務操作,就要寫多少重復的校驗和日志記錄代碼,存在大量的冗余代碼,這顯然是無法接受的。如果你稍微覺得不妥,在原來的基礎上進行一次優化,利用面向對象的思想,將這些冗余的代碼抽離出來做成一個公共的方法,示意圖如下:
這樣的確是可以解決代碼冗余和可維護性的問題,但是我們在使用的時候依然需要手動的調用這些公共的方法,看起來也有點不妥。那么有沒有一種更好的方法,我們不需要每次都去調用,在調用方法前后就會自動幫我們進行對應的操作呢?答案是肯定的,用AOP就可以,AOP將權限校驗、日志記錄等非業務代碼完全提取出來,與業務代碼分離,并尋找節點切入業務代碼中:
AOP體系
AOP的體系大致如下圖:
接下來對各個概念做一下解釋:
Aspect
切面,包含Pointcut和Advice。在使用AOP進行切面定義的時候,在類上加上@Aspect即可定義一個切面類。例如我們需要定義一個日志切面LogAspect,則可如下定義,@Component 注解表示該類交給 Spring 來管理。
/*** @Author likangmin* @create 2020/11/24 17:03*/ //定義切面類LogAspect @Aspect @Component public class LogAdvice {}Pointcut
切點,決定處理如權限校驗、日志記錄等在何處切入業務代碼中(即織入切面),分為execution(系統注解,可以用路徑表達式指定哪些類織入切面)方式和annotation(自定義注解,以指定被哪些注解修飾的代碼織入切面)方式。在實際使用中,用@Pointcut注解定義一個切面,即某件事情的入口,如記錄操作日志的入口,切入點定義了事件觸發時機。
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component public class LogAspect{/*** 定義一個切面,攔截 com.kmli.aopexe.controller 包和子包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {} }以上表示攔截com.kmli.aopexe.controller 包和子包下的所有方法,進入該包和子包下的所有方法都需要進行記錄日志的操作。前面說了兩種注解的方式,接下里解釋一下兩種方式中表達式的具體含義:
execution方式
以代碼中execution(* com.kmli.aopexe.controller….(…))為例:
- 第一個 *號的位置:表示返回值類型,星號表示所有類型;
- com.kmli.aopexe.controller…: 表示需要攔截的包名,后面的兩個句點表示當前包和當前包的所有子包;
- 第二個 * 號的位置:表示類名,* 表示所有類;
- *(…):星號表示所有的方法,后面括弧里面表示方法的參數,兩個句點表示任何參數。
annotation方式
這種方式是針對某個注解來定義切面,比如我們對具有 @PostMapping 注解的方法做切面,可以如下定義切面:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void annotationPointcut() {}使用該切面的話,就會切入注解是 @PostMapping 的所有方法。這種方式很適合處理 @GetMapping、@PostMapping、@DeleteMapping不同注解有各種特定處理邏輯的場景。
Advice
處理,包括處理時機(在什么時機執行處理內容,分為前置處理(即業務代碼執行前)、后置處理(業務代碼執行后)等)和處理內容(要做什么事,比如校驗權限和記錄日志)。
處理內容沒有什么需要說明,主要看看處理時機的幾個注解:
@Before
使用@Before 注解指定的方法在切面切入目標方法之前執行,這個時候我們可以做一些處理,如記錄當前調用日志,獲取用戶請求的URL以及用戶的IP等等信息。還是以上面的日志記錄為例介紹一下使用方法(參數JointPoint對象很重要):
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component @Slf4j public class LogAdvice {/*** 定義一個切面,攔截 com.mutest.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定義的切面方法之前執行該方法* @param joinPoint jointPoint*/@Before("pointCut()")public void doBefore(JoinPoint joinPoint) {log.info("====doBefore方法進入了====");// 獲取簽名Signature signature = joinPoint.getSignature();// 獲取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 獲取即將執行的方法名String funcName = signature.getName();log.info("即將執行方法為: {},屬于{}包", funcName, declaringTypeName);// 也可以用來記錄一些信息,比如獲取請求的 URL 和 IPServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 獲取請求 URLString url = request.getRequestURL().toString();// 獲取請求 IPString ip = request.getRemoteAddr();log.info("用戶請求的url為:{},ip地址為:{}", url, ip);log.info("====doBefore方法結束了====");} }@After
和 @Before 注解相對應,指定的方法在切面切入目標方法之后執行,同樣可以做和@Before一樣的處理:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component @Slf4j public class LogAdvice {/*** 定義一個切面,攔截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在上面定義的切面方法之后執行該方法* @param joinPoint jointPoint*/@After("pointCut()")public void doAfter(JoinPoint joinPoint) {log.info("==== doAfter 方法進入了====");Signature signature = joinPoint.getSignature();String method = signature.getName();log.info("方法{}已經執行完", method);log.info("==== doAfter 方法結束了====");} }@Around
用于修飾Around增強處理,可以自由選擇增強動作與目標方法的執行順序,也就是說可以在增強動作前后,甚至過程中執行目標方法,之所以具有這樣的特性主要是因為使用了用ProceedingJoinPoint參數的procedd()方法才會執行目標方法,這就是@Around增強處理可以完全控制目標方法執行時機、如何執行的關鍵,如果程序沒有調用ProceedingJoinPoint的proceed方法,則目標方法不會執行。我們在使用@Around時,對應的方法的第一個形參必須是 ProceedingJoinPoint 類型(至少一個形參),正如上面所說ProceedingJoinPoint.procedd()的重要性。
使用它不僅可以改變執行目標方法的參數值,也可以改變執行目標方法之后的返回值,具體實現方法是調用ProceedingJoinPoint的proceed方法時,還可以傳入一個Object[ ]對象,該數組中的值將被傳入目標方法作為實參,需要注意的是如果傳入的Object[ ]數組長度與目標方法所需要的參數個數不相等,或者Object[ ]數組元素與目標方法所需參數的類型不匹配,程序就會出現異常。
注意:
具體看一下使用的示例:
首先定義一個測試接口類TestController,定義方法getGroupList用于檢驗權限:
然后我們定義一個切面類:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @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("===================開始增強處理===================");//獲取請求參數,詳見接口類Object[] objects = joinPoint.getArgs();Long id = ((JSONObject) objects[0]).getLong("id");String name = ((JSONObject) objects[0]).getString("name");// 修改入參JSONObject object = new JSONObject();object.put("id", 8);object.put("name", "kmli");objects[0] = object;// 將修改后的參數傳入return joinPoint.proceed(objects);} }使用PostMan進行調用,傳入參數{“id”:-5,“name”:“admin”},返回如下:
{"code":200,"data":{"name":"kmli","id":"8"},"message":"SUCCESS"}從結果可以看出,@Around截取到了接口的入參,并使接口返回了切面類中的結果。
@AfterReturning
@AfterReturning 注解和 @After 有些類似,區別在于 @AfterReturning 注解可以用來捕獲切入方法執行完之后的返回值,對返回值進行業務邏輯上的增強處理。還是以日志為例:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component @Slf4j public class LogAdvice {/*** 定義一個切面,攔截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定義的切面方法返回后執行該方法,可以捕獲返回對象或者對返回對象進行增強* @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("方法{}執行完畢,返回參數為:{}", classMethod, result);// 實際項目中可以根據業務做具體的返回值增強log.info("對返回參數進行業務上的增強:{}", result + "需要增加的信息");} }但是在使用@AfterReturning 注解時需要注意的是返回的值必須和參數保持一致,否則會報錯。
@AfterThrowing
當被切方法執行過程中拋出異常時,會進入 @AfterThrowing 注解的方法中執行,在該方法中可以做一些異常的處理邏輯。還是以日志為例:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component @Slf4j public class LogAdvice {/*** 定義一個切面,攔截 com.kmli.aopexe.controller 包下的所有方法*/@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")public void pointCut() {}/*** 在定義的切面方法執行拋異常時,執行該方法* @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("執行方法{}出錯,異常為:{}", method, ex);} }Joint point
連接點,是程序執行的一個點,一個方法的執行或者一個異常的處理都是一個連接點。在 Spring AOP 中,一個連接點總是代表一個方法執行。
Weaving
織入,通過動態代理,在目標對象方法中執行處理內容的過程。
AOP使用
對AOP進行理論和參數進行分析之后,接下來我們使用AOP進行一下實際的操作。首先在使用AOP之前需要在項目的POM文件中引入AOP的相關依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>還有一個版本屬性,不寫就自動為最新的版本。既然我們之前以日志和權限進行說明,所以我們還是拿這兩個進行示例說明。可能跟上面的代碼會有重復的地方。先來看我們的第一個需求:所有的get請求被調用前需要記錄信息"get請求的advice觸發了"。則我們定義切面類如下:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component public class LogAdvice {// 定義一個切點:所有被GetMapping注解修飾的方法會織入advice@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")private void logAdvicePointcut() {}// Before表示logAdvice將在目標方法執行前執行@Before("logAdvicePointcut()")public void logAdvice(){// 這里只是一個示例,你可以寫任何處理邏輯System.out.println("get請求的advice觸發了");} }然后在測試類中寫post類的接口:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @RestController @RequestMapping(value = "/aop") public class AopController {@PostMapping(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}");} }項目啟動后,使用postMan調用http://localhost:8085/aop/getTest,會發現控制臺打印了:get請求的advice觸發了,當調用http://localhost:8085/aop/postTest時,沒有打印任何信息。
下面我們將問題復雜化一些,我們的需求是自定義一個注解,要求只要是標注了自定義注解的方法都需要對參數進行檢驗,這個可以怎么做呢?
首先自定義一個注解,對于自定義注解這里不做過多解釋,不太懂的伙伴可以百度查看,不復雜。我們自定義注解類PermissionsAnnotation:
然后創建一個切面用于參數的校驗:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @Aspect @Component @Order(1) public class PermissionAdvice {// 定義一個切面,括號內寫入第1步中自定義注解的路徑@Pointcut("@annotation(com.kmli.demo.annotation.PermissionAnnotation)")private void permissionCheck() {}@Around("permissionCheck()")public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {Object[] objects = joinPoint.getArgs();Long id = ((JSONObject) objects[0]).getLong("id");String name = ((JSONObject) objects[0]).getString("name");// id小于0則拋出非法id的異常if (id < 0) {return JSON.parseObject("{"message":"illegal id","code":403}");}return joinPoint.proceed();} }然后在測試類中寫接口:
/*** @Author likangmin* @create 2020/11/24 17:03*/ @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}");}@RequestMapping(value = "/check2", method = RequestMethod.POST)public JSONObject getGroupList2(@RequestBody JSONObject request) {return JSON.parseObject("{"message":"SUCCESS","code":200}");} }然后再利用postMan進行測試,輸入http://localhost:8085/permission/check,傳入參數{“id”:1,“name”:“kmli”},則正常返回:
{"code":200,"message":"SUCCESS"}然后我們傳入參數{“id”:1,“name”:“kmli”},則返回:
{"code":403,"message":"illegal id"}然后我們使用同樣的參數,調用http://localhost:8085/permission/check2,不論哪種傳參都是正常返回。
到這里可能有人會問,如果我一個接口想設置多個切面類進行校驗怎么辦?如果我想切面1在切面2之前執行怎么辦呢?這些切面的執行順序如何管理?很簡單,一個自定義的AOP注解可以對應多個切面類,這些切面類執行順序由@Order注解管理,該注解后的數字越小,所在切面類越先執行。
以上就是要講的AOP的全部內容,通過幾個簡單的例子就可以感受到,AOP的便捷之處。如果有不足的地方還請大家指出,感謝大家的閱讀。
總結
以上是生活随笔為你收集整理的SpringBoot-切面AOP实现统一逻辑处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决SQL注入与XSS攻击
- 下一篇: Redis专题-底层数据结构与使用场景