背景:java
我須要在一個SpringBoot的項目中的每一個controller加入一個日誌記錄,記錄關於請求的一些信息。web
代碼相似於:spring
logger.info(request.getRequestUrl());編程
之類的。瀏覽器
代碼不難,但因爲Controller的數量很多,幹起來也是體力活。因此想到了用Spring AOP來解決這個問題。session
首先,在pom中加入SpringAOP的相關依賴:mvc
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
上一篇咱們說到,若是要直接用@Aspect註解的話,要在spring的配置文件中加入框架
<aop:aspectj-autoproxy />spring-boot
那麼咱們這裏要不要在程序的主類中增長@EnableAspectJAutoProxy來啓用呢? 實際並不須要,能夠看下面關於AOP的默認配置屬性,其中spring.aop.auto屬性默認是開啓的,也就是說只要引入了AOP依賴後,默認已經增長了@EnableAspectJAutoProxy。ui
好的也就是說,只要引入SpringAOP相關的jar包依賴,咱們就能夠開始相關的Aspet的編程了。
這裏直接上代碼,而後再作解釋:
首先是包結構的圖:
這裏涉及到接收請求的Controller的包有兩個,com.stuPayment.controller還有com.stuPayment.uiController
而後看咱們的切面類WebLogAspect類的代碼:
package com.stuPayment.util; import java.util.Arrays; import javax.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @Aspect @Component public class WebLogAspect { private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.stuPayment.controller..*.*(..))")//切入點描述 這個是controller包的切入點 public void controllerLog(){}//簽名,能夠理解成這個切入點的一個名稱 @Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入點描述,這個是uiController包的切入點 public void uiControllerLog(){} @Before("controllerLog() || uiControllerLog()") //在切入點的方法run以前要乾的 public void logBeforeController(JoinPoint joinPoint) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//這個RequestContextHolder是Springmvc提供來得到請求的東西 HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest(); // 記錄下請求內容 logger.info("################URL : " + request.getRequestURL().toString()); logger.info("################HTTP_METHOD : " + request.getMethod()); logger.info("################IP : " + request.getRemoteAddr()); logger.info("################THE ARGS OF THE CONTROLLER : " + Arrays.toString(joinPoint.getArgs())); //下面這個getSignature().getDeclaringTypeName()是獲取包+類名的 而後後面的joinPoint.getSignature.getName()獲取了方法名 logger.info("################CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); //logger.info("################TARGET: " + joinPoint.getTarget());//返回的是須要增強的目標類的對象 //logger.info("################THIS: " + joinPoint.getThis());//返回的是通過增強後的代理類的對象 } }
針對這個切面類,來展開說明@Aspect切面類的編程。
@Aspect和@Component
首先,這個@Aspect註釋告訴Spring這是個切面類,而後@Compoment將轉換成Spring容器中的bean或者是代理bean。 總之要寫切面這兩個註解一塊兒用就是了。
既然是切面類,那麼確定是包含PointCut還有Advice兩個要素的,下面對幾個註解展開講來看看在@Aspect中是怎麼肯定切入點(PointCut)和加強通知(Advice)的。
@PointCut
這個註解包含兩部分,PointCut表達式和PointCut簽名。表達式是拿來肯定切入點的位置的,說白了就是經過一些規則來肯定,哪些方法是要加強的,也就是要攔截哪些方法。
@PointCut(...........)括號裏面那些就是表達式。這裏的execution是其中的一種匹配方式,還有:
execution: 匹配鏈接點 within: 某個類裏面 this: 指定AOP代理類的類型 target:指定目標對象的類型 args: 指定參數的類型 bean:指定特定的bean名稱,可使用通配符(Spring自帶的) @target: 帶有指定註解的類型 @args: 指定運行時傳的參數帶有指定的註解 @within: 匹配使用指定註解的類 @annotation:指定方法所應用的註解
注意,因爲是動態代理的實現方法,因此不是全部的方法都能攔截得下來,對於JDK代理只有public的方法才能攔截得下來,對於CGLIB只有public和protected的方法才能攔截。
這裏咱們主要介紹execution的匹配方法,由於大多數時候都會用這個來定義pointcut:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) execution(方法修飾符(可選) 返回類型 類路徑 方法名 參數 異常模式(可選))
除了返回類型,方法名還有參數以外,其餘都是可選的
ret-type-pattern:能夠爲*表示任何返回值,全路徑的類名等.
name-pattern:指定方法名,*表明因此,set*,表明以set開頭的全部方法.
parameters pattern:指定方法參數(聲明的類型), ()匹配沒有參數; (..)表明任意多個參數; (*)表明一個參數,但能夠是任意型; (*,String)表明第一個參數爲任何值,第二個爲String類型。
下面給幾個例子:
1)execution(public * *(..))——表示匹配全部public方法 2)execution(* set*(..))——表示全部以「set」開頭的方法 3)execution(* com.xyz.service.AccountService.*(..))——表示匹配全部AccountService接口的方法 4)execution(* com.xyz.service.*.*(..))——表示匹配service包下全部的方法 5)execution(* com.xyz.service..*.*(..))——表示匹配service包和它的子包下的方法
而後其餘的匹配法要用的時候再百度吧~
而後是@PointCut的第二個部分,簽名signature,也就是代碼中的
@Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入點描述,這個是uiController包的切入點 public void uiControllerLog(){}
像方法定義的這個Public void uiControllerLog(){}這個看起來像是方法定義的東西,就是簽名,簽名沒有實際用處,只是用來標記一個Pointcut,能夠理解成這個切入點的一個記號。
@Before
這個是決定advice在切入點方法的什麼地方執行的標籤,這個註解的意思是在切入點方法執行以前執行咱們定義的advice。
@Before("controllerLog() || uiControllerLog()") //在切入點的方法run以前要乾的 public void logBeforeController(JoinPoint joinPoint) {
@Before註解括號裏面寫的是一個切入點,這裏看見切入點表達式能夠用邏輯符號&&,||,!來描述。 括號裏面也能夠內置切點表達式,也就是直接寫成:
@Before("execution(public * com.stuPayment.uiController..*.*(..))")
跟寫成@Before("uiControllerLog()")的效果是同樣的。
而後看到註解下面的方法,就是描述advice的,咱們看到有個參數JoinPoint,這個東西表明着織入加強處理的鏈接點。JoinPoint包含了幾個頗有用的參數:
除了註解@Around的方法外,其餘均可以加這個JoinPoint做參數。@Around註解的方法的參數必定要是ProceedingJoinPoint,下面會介紹。
@After
這個註解就是在切入的方法運行完以後把咱們的advice加強加進去。同樣方法中能夠添加JoinPoint。
@Around
這個註解能夠簡單地看做@Before和@After的結合。這個註解和其餘的比比較特別,它的方法的參數必定要是ProceedingJoinPoint,這個對象是JoinPoint的子類。咱們能夠把這個看做是切入點的那個方法的替身,這個proceedingJoinPoint有個proceed()方法,至關於就是那切入點的那個方法執行,簡單地說就是讓目標方法執行,而後這個方法會返回一個對象,這個對象就是那個切入點所在位置的方法所返回的對象。
除了這個Proceed方法(很重要的方法),其餘和那幾個註解同樣。
@AfterReturning
顧名思義,這個註解是在目標方法正常完成後把加強處理織入。這個註解能夠指定兩個屬性(以前的三個註解後面的括號只寫一個@PointCut表達式,也就是隻有一個屬性),一個是和其餘註解同樣的PointCut表達式,也就是描述該advice在哪一個接入點被織入;而後還能夠有個returning屬性,代表能夠在Advice的方法中有目標方法返回值的形參。
@AfterReturning(returning = "returnOb", pointcut = "controllerLog() || uiControllerLog()") public void doAfterReturning(JoinPoint joinPoint, Object returnOb) { System.out.println("##################### the return of the method is : " + returnOb); }
瀏覽器發出一個請求後,效果截圖:
(這裏是一個請求登陸界面的請求,因此uicontroller返回一個String做爲視圖。)
@AfterThrowing
異常拋出加強,在異常拋出後織入的加強。有點像上面的@AfterReturning,這個註解也是有兩個屬性,pointcut和throwing。
用法也和剛剛的那個returning差很少:
@AfterThrowing(pointcut = "controllerLog() || uiControllerLog()", throwing = "ex") public void doAfterThrowing(JoinPoint joinPoint, Exception ex) { String methodName = point.getSignature().getName(); List<Object> args = Arrays.asList(point.getArgs()); System.out.println("鏈接點方法爲:" + methodName + ",參數爲:" + args + ",異常爲:" + ex); }
好了如今註解都介紹完了,這裏還要提到上面用到的一個類:RequestContextHolder
好比說,有個需求須要在service中得到request和response,咱們通常會(我就是)直接在controller那把request或response做爲參數傳到service,這就很不美觀。後來知道,原來SpringMVC提供了個很強大的類ReqeustContextHolder,經過他你就能夠得到request和response什麼的。
//下面兩個方法在沒有使用JSF的項目中是沒有區別的 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); // RequestContextHolder.getRequestAttributes(); //從session裏面獲取對應的值 String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION); HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest(); HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();
好了完成了這個切面的編程後,你就成功把日誌功能切入到各個controller中了。看個效果圖。
最後,再記錄一下各個不一樣的advice的攔截順序的問題。
狀況一,只有一個Aspect類:
無異常:@Around(proceed()以前的部分) → @Before → 方法執行 → @Around(proceed()以後的部分) → @After → @AfterReturning
有異常:@Around(proceed(以前的部分)) → @Before → 扔異常ing → @After → @AfterThrowing (大概是由於方法沒有跑完拋了異常,沒有正確返回全部@Around的proceed()以後的部分和@AfterReturning兩個註解的增強沒有可以織入)
狀況二,同一個方法有多個@Aspect類攔截:
單個Aspect確定是和只有一個Aspect的時候的狀況是同樣的,但不一樣的Aspect裏面的advice的順序呢??答案是不必定,像是線程同樣,沒有誰先誰後,除非你給他們分配優先級,一樣地,在這裏你也能夠爲@Aspect分配優先級,這樣就能夠決定誰先誰後了。
優先級有兩種方式:
不論是哪一種,都是order的值越小越先執行:
@Order(5) @Component @Aspect public class Aspect1 { // ... } @Order(6) @Component @Aspect public class Aspect2 { // ... }
這樣Aspect1就永遠比Aspect2先執行了。
注意點:
參考過的博客:
https://blog.csdn.net/bombsklk/article/details/79143145 SpringBoot用AOP實現日誌
https://blog.csdn.net/loongshawn/article/details/72303040?utm_source=tuicool&utm_medium=referral execution表達式
https://blog.csdn.net/caomiao2006/article/details/51287200 advice中獲取參數的方法
https://blog.csdn.net/rainbow702/article/details/52185827 advice的順序
https://blog.csdn.net/zzy7075/article/details/53559902 RequestContextHolder類的相關介紹