SpringAop日志找不到方法的處理

SpringAop日志找不到方法

錯誤截圖:

在這裡插入圖片描述

顯示沒有找到該方法,於是我找到對應的類和對應的方法:

在這裡插入圖片描述

這裡我用瞭反射來獲取方法名和參數:

在這裡插入圖片描述

錯誤打印的結果顯示方法名獲取沒有錯誤,於是我查看參數的類型是否有錯

結果一個都對不上…

int類型反射得到的class:

在這裡插入圖片描述

在這裡插入圖片描述

Integer反射得到的Class:

在這裡插入圖片描述

在這裡插入圖片描述

…終於知道之前錯誤裡的Ljavexxxx是哪裡來的瞭…

由於model是一個接口

在這裡插入圖片描述

model反射的Class得到的是他的子類org.springframework.validation.support.BindingAwareModelMap:

在這裡插入圖片描述

所以參數類型對不上號導致瞭錯誤的產生

解決方法:

將int類型的參數改為Integer(以後都寫Integer,舍棄int!),將參數裡的Model刪去,將方法返回值改為ModeAndView,一樣實現頁面的跳轉和參數的傳遞:

在這裡插入圖片描述

運行代碼:

在這裡插入圖片描述

運行okkkk~

SpringBoot用SpringAOP實現日志記錄功能

背景:

我需要在一個SpringBoot的項目中的每個controller加入一個日志記錄,記錄關於請求的一些信息。

代碼類似於:

logger.info(request.getRequestUrl());

之類的。

代碼不難,但由於Controller的數量不少,幹起來也是體力活。所以想到瞭用Spring AOP來解決這個問題。

首先,在pom中加入SpringAOP的相關依賴:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

上一篇我們說到,如果要直接用@Aspect註解的話,要在spring的配置文件中加入

<aop:aspectj-autoproxy />

那麼我們這裡要不要在程序的主類中增加@EnableAspectJAutoProxy來啟用呢? 實際並不需要,可以看下面關於AOP的默認配置屬性,其中spring.aop.auto屬性默認是開啟的,也就是說隻要引入瞭AOP依賴後,默認已經增加瞭

@EnableAspectJAutoProxy

好的也就是說,隻要引入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包含瞭幾個很有用的參數:

  • Object[] getArgs:返回目標方法的參數
  • Signature getSignature:返回目標方法的簽名
  • Object getTarget:返回被織入增強處理的目標對象
  • Object getThis:返回AOP框架為目標對象生成的代理對象

除瞭註解@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分配優先級,這樣就可以決定誰先誰後瞭。

優先級有兩種方式:

  • 實現org.springframework.core.Ordered接口,實現它的getOrder()方法
  • 給aspect添加@Order註解,該註解全稱為:org.springframework.core.annotation.Order

不管是哪種,都是order的值越小越先執行:

@Order(5)
@Component
@Aspect
public class Aspect1 {
    // ...
}
@Order(6)
@Component
@Aspect
public class Aspect2 {
    // ...
}

這樣Aspect1就永遠比Aspect2先執行瞭。

註意點:

  • 如果在同一個 aspect 類中,針對同一個 pointcut,定義瞭兩個相同的 advice(比如,定義瞭兩個 @Before),那麼這兩個 advice 的執行順序是無法確定的,哪怕你給這兩個 advice 添加瞭 @Order 這個註解,也不行。這點切記。
  • 對於@Around這個advice,不管它有沒有返回值,但是必須要方法內部,調用一下 pjp.proceed();否則,Controller 中的接口將沒有機會被執行,從而也導致瞭 @Before這個advice不會被觸發。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: