Spring Boot 中的切面AOP處理
本來已收錄到我寫的10萬字Springboot經(jīng)典學習筆記中,筆記在持續(xù)更新……文末有領取方式
1. 什么是AOP
AOP:Aspect Oriented Programming 的縮寫,意為:面向切面編程。面向切面編程的目標就是分離關注點。什么是關注點呢?就是關注點,就是你要做的事情。假如你是一位公子哥,沒啥人生目標,每天衣來伸手,飯來張口,整天只知道一件事:玩(這就是你的關注點,你只要做這一件事)!但是有個問題,你在玩之前,你還需要起床、穿衣服、穿鞋子、疊被子、做早飯等等等等,但是這些事情你不想關注,也不用關注,你只想想玩,那么怎么辦呢?
對!這些事情通通交給下人去干。你有一個專門的仆人 A 幫你穿衣服,仆人 B 幫你穿鞋子,仆人 C 幫你疊好被子,仆人 D 幫你做飯,然后你就開始吃飯、去玩(這就是你一天的正事),你干完你的正事之后,回來,然后一系列仆人又開始幫你干這個干那個,然后一天就結束了!
這就是 AOP。AOP 的好處就是你只需要干你的正事,其它事情別人幫你干。也許有一天,你想裸奔,不想穿衣服,那么你把仆人 A 解雇就是了!也許有一天,出門之前你還想帶點錢,那么你再雇一個仆人 E 專門幫你干取錢的活!這就是AOP。每個人各司其職,靈活組合,達到一種可配置的、可插拔的程序結構。
2. Spring Boot 中的 AOP 處理
2.1 AOP 依賴
使用AOP,首先需要引入AOP的依賴。
<dependency>
?<groupId>org.springframework.bootgroupId>
?<artifactId>spring-boot-starter-aopartifactId>
dependency>
2.2 實現(xiàn) AOP 切面
Spring Boot 中使用 AOP 非常簡單,假如我們要在項目中打印一些 log,在引入了上面的依賴之后,我們新建一個類 LogAspectHandler,用來定義切面和處理方法。只要在類上加個@Aspect注解即可。@Aspect 注解用來描述一個切面類,定義切面類的時候需要打上這個注解。@Component 注解讓該類交給 Spring 來管理。
@Aspect
@Component
public?class?LogAspectHandler?{
}
這里主要介紹幾個常用的注解及使用:
1.@Pointcut:定義一個切面,即上面所描述的關注的某件事入口。
2.@Before:在做某件事之前做的事。
3.@After:在做某件事之后做的事。
4.@AfterReturning:在做某件事之后,對其返回值做增強處理。
5.@AfterThrowing:在做某件事拋出異常時,處理。
2.2.1 @Pointcut 注解
@Pointcut 注解:用來定義一個切面(切入點),即上文中所關注的某件事情的入口。切入點決定了連接點關注的內容,使得我們可以控制通知什么時候執(zhí)行。
@Aspect
@Component
public?class?LogAspectHandler?{
????/**
?????*?定義一個切面,攔截com.itcodai.course09.controller包和子包下的所有方法
?????*/
????@Pointcut("execution(*?com.itcodai.course09.controller..*.*(..))")
????public?void?pointCut()?{}
}
@Pointcut 注解指定一個切面,定義需要攔截的東西,這里介紹兩個常用的表達式:一個是使用 execution(),另一個是使用 annotation()。
以 execution(* com.itcodai.course09.controller..*.*(..))) 表達式為例,語法如下:
execution()為表達式主體
第一個*號的位置:表示返回值類型,*表示所有類型
包名:表示需要攔截的包名,后面的兩個句點表示當前包和當前包的所有子包,com.itcodai.course09.controller包、子包下所有類的方法
第二個*號的位置:表示類名,*表示所有類*(..):這個星號表示方法名,*表示所有的方法,后面括弧里面表示方法的參數(shù),兩個句點表示任何參數(shù)
annotation() 方式是針對某個注解來定義切面,比如我們對具有@GetMapping注解的方法做切面,可以如下定義切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public?void?annotationCut()?{}
然后使用該切面的話,就會切入注解是 @GetMapping 的方法。因為在實際項目中,可能對于不同的注解有不同的邏輯處理,比如 @GetMapping、@PostMapping、@DeleteMapping 等。所以這種按照注解的切入方式在實際項目中也很常用。
2.2.2 @Before 注解
@Before 注解指定的方法在切面切入目標方法之前執(zhí)行,可以做一些 log 處理,也可以做一些信息的統(tǒng)計,比如獲取用戶的請求 url 以及用戶的 ip 地址等等,這個在做個人站點的時候都能用得到,都是常用的方法。例如:
@Aspect
@Component
public?class?LogAspectHandler?{
????private?final?Logger?logger?=?LoggerFactory.getLogger(this.getClass());
????/**
?????*?在上面定義的切面方法之前執(zhí)行該方法
?????*?@param?joinPoint?jointPoint
?????*/
????@Before("pointCut()")
????public?void?doBefore(JoinPoint?joinPoint)?{
????????logger.info("====doBefore方法進入了====");
????????//?獲取簽名
????????Signature?signature?=?joinPoint.getSignature();
????????//?獲取切入的包名
????????String?declaringTypeName?=?signature.getDeclaringTypeName();
????????//?獲取即將執(zhí)行的方法名
????????String?funcName?=?signature.getName();
????????logger.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();
????????logger.info("用戶請求的url為:{},ip地址為:{}",?url,?ip);
????}
}
JointPoint 對象很有用,可以用它來獲取一個簽名,然后利用簽名可以獲取請求的包名、方法名,包括參數(shù)(通過 joinPoint.getArgs() 獲取)等等。
2.2.3 @After 注解
@After 注解和 @Before ?注解相對應,指定的方法在切面切入目標方法之后執(zhí)行,也可以做一些完成某方法之后的 log 處理。
@Aspect
@Component
public?class?LogAspectHandler?{
????private?final?Logger?logger?=?LoggerFactory.getLogger(this.getClass());
????/**
?????*?定義一個切面,攔截com.itcodai.course09.controller包下的所有方法
?????*/
????@Pointcut("execution(*?com.itcodai.course09.controller..*.*(..))")
????public?void?pointCut()?{}
????/**
?????*?在上面定義的切面方法之后執(zhí)行該方法
?????*?@param?joinPoint?jointPoint
?????*/
????@After("pointCut()")
????public?void?doAfter(JoinPoint?joinPoint)?{
????????logger.info("====doAfter方法進入了====");
????????Signature?signature?=?joinPoint.getSignature();
????????String?method?=?signature.getName();
????????logger.info("方法{}已經(jīng)執(zhí)行完",?method);
????}
}
到這里,我們來寫一個 Controller 來測試一下執(zhí)行結果,新建一個 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.course09.controller.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 兩個注解的實際作用。
2.2.4 @AfterReturning 注解
@AfterReturning 注解和 @After 有些類似,區(qū)別在于 @AfterReturning 注解可以用來捕獲切入方法執(zhí)行完之后的返回值,對返回值進行業(yè)務邏輯上的增強處理,例如:
@Aspect
@Component
public?class?LogAspectHandler?{
????private?final?Logger?logger?=?LoggerFactory.getLogger(this.getClass());
????/**
?????*?在上面定義的切面方法返回后執(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();
????????logger.info("方法{}執(zhí)行完畢,返回參數(shù)為:{}",?classMethod,?result);
????????//?實際項目中可以根據(jù)業(yè)務做具體的返回值增強
????????logger.info("對返回參數(shù)進行業(yè)務上的增強:{}",?result?+?"增強版");
????}
}
需要注意的是:在 @AfterReturning注解 中,屬性 returning 的值必須要和參數(shù)保持一致,否則會檢測不到。該方法中的第二個入?yún)⒕褪潜磺蟹椒ǖ姆祷刂担?doAfterReturning 方法中可以對返回值進行增強,可以根據(jù)業(yè)務需要做相應的封裝。我們重啟一下服務,再測試一下(多余的 log 我不貼出來了):
方法testAop執(zhí)行完畢,返回參數(shù)為:Hello CSDN ?
對返回參數(shù)進行業(yè)務上的增強:Hello CSDN增強版
2.2.5 @AfterThrowing 注解
顧名思義,@AfterThrowing 注解是當被切方法執(zhí)行時拋出異常時,會進入 @AfterThrowing 注解的方法中執(zhí)行,在該方法中可以做一些異常的處理邏輯。要注意的是 throwing 屬性的值必須要和參數(shù)一致,否則會報錯。該方法中的第二個入?yún)⒓礊閽伋龅漠惓!?/p>
/**
?*?使用AOP處理log
?*?@author?shengwu?ni
?*?@date?2018/05/04?20:24
?*/
@Aspect
@Component
public?class?LogAspectHandler?{
????private?final?Logger?logger?=?LoggerFactory.getLogger(this.getClass());
????/**
?????*?在上面定義的切面方法執(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();
????????//?處理異常的邏輯
????????logger.info("執(zhí)行方法{}出錯,異常為:{}",?method,?ex);
????}
}
該方法我就不測試了,大家可以自行測試一下。
3. 總結
本節(jié)課針對 Spring Boot 中的切面 AOP 做了詳細的講解,主要介紹了 Spring Boot 中 AOP 的引入,常用注解的使用,參數(shù)的使用,以及常用 api 的介紹。AOP 在實際項目中很有用,對切面方法執(zhí)行前后都可以根據(jù)具體的業(yè)務,做相應的預處理或者增強處理,同時也可以用作異常捕獲處理,可以根據(jù)具體業(yè)務場景,合理去使用 AOP。
該文已收錄到我寫的《10萬字Springboot經(jīng)典學習筆記》中,點擊下面小卡片,進入【Java開發(fā)寶典】,回復:筆記,即可免費獲取。
點贊是最大的支持?![]()
