在SpringBoot中使用Aop功能實現日誌功能

實現背景

主要是爲了熟悉Aop的主要註解及功能,給項目的Controller層加上日誌

首先在pom文件裏面加入aop依賴

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

相比入ssm,springboot簡化了很多的註解
直接看切面類

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
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;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 爲controller層編寫了一個切面aop日誌文件
 *
 * @Aspect表示這是一個切面
 * @Component
 */
@Aspect
@Component
public class WebLogAspect {
    private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    /**
     * 定義一個controller包的切入點,包含表達式和表達式簽名
     * execution(方法修飾符(可選)  返回類型  類路徑 方法名  參數  異常模式(可選))
     */
    @Pointcut("execution(public * com.springboot.richttms.controller..*.*(..))")
    public void controllerLog(){ }//這個方法其實就是簽名,沒有實際用處,用來標記一個pointcut

    //在切入點的run方法之前執行這個方法
    @Before("controllerLog()")
    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());//返回的是經過加強後的代理類的對象
    }
	//返回參數的返回值
    @AfterReturning(pointcut = "controllerLog()",returning = "returnOb")
    public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
        System.out.println("##################### the return of the method is : " + returnOb.toString());
    }
}

打印出來的日誌

2019-07-27 16:08:16.455  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################URL : http://localhost:8080/scheduleController/schedule/8813
2019-07-27 16:08:16.455  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################HTTP_METHOD : GET
2019-07-27 16:08:16.456  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################IP : 0:0:0:0:0:0:0:1
2019-07-27 16:08:16.456  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################THE ARGS OF THE CONTROLLER : [8813]
2019-07-27 16:08:16.459  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################CLASS_METHOD : com.springboot.richttms.controller.schedule.scheduleController.queryBycId
2019-07-27 16:08:16.826  INFO 13112 --- [nio-8080-exec-2] io.lettuce.core.EpollProvider            : Starting without optional epoll library
2019-07-27 16:08:16.829  INFO 13112 --- [nio-8080-exec-2] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
##################### the return of the method is : Data{data={movie=[Movie{movieId=346559, movieName='黑衣人:全球追緝', movieType='動作', movieActor='克里斯·海姆斯沃斯,泰莎·湯普森,麗貝卡·弗格森', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/moviemachine/262f95bad79b6ae45b978593157cb68550938.jpg'}, Movie{movieId=1204589, movieName='絕殺慕尼黑', movieType='動作', movieActor='弗拉基米爾·馬什科夫,約翰·薩維奇,伊萬·科列斯尼科夫', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/movie/67044d5479f075a18adba35571cadc4f978021.jpg'}, Movie{movieId=1207185, movieName='秦明·生死語者', movieType='懸疑', movieActor='嚴屹寬,代斯,耿樂', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/movie/313ed0afcdb4d1cc37cd12b402e9e4421137394.jpg'}]}}

@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,

public void controllerLog(){ }//這個方法其實就是簽名,沒有實際用處,用來標記一個pointcut

@Before
這個是決定advice在切入點方法的什麼地方執行的標籤,這個註解的意思是在切入點方法執行之前執行我們定義的advice

@Before("controllerLog()")
    public void logBeforeController(JoinPoint joinPoint){} 

@Before註解括號裏面寫的是一個切入點,這裏看見切入點表達式可以用邏輯符號&&,||,!來描述。 括號裏面也可以內置切點表達式,也就是直接寫成:

@Before("execution(public * com.springboot.richttms.controller..*.*(..))")

然後看到註解下面的方法,就是描述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(pointcut = "controllerLog()",returning = "returnOb")
    public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
        System.out.println("##################### the return of the method is : " + returnOb.toString());
    }

再記錄一下各個不同的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 {
    // ...
}

定義一個自定義的註解類

import java.lang.annotation.*;

/**
 * 自定義一個註解類(連接點)
 */
@Target(ElementType.METHOD)//指明瞭修飾的這個註解的使用範圍,具體根據ElementType來定
@Retention(RetentionPolicy.RUNTIME)//指明修飾註解的生存週期
@Documented//指明修飾的註解,可以被例如javadoc此類的工具文檔化,只負責標記,沒有成員取值。
@Inherited//y允許子類繼承父類
public @interface Log {

    /**
     * 方法名
     */
    public String name() default "";

    /**
     * 描述
     */
    public String description() default "no description";

}

@Target 註解
功能:指明瞭修飾的這個註解的使用範圍,即被描述的註解可以用在哪裏。

ElementType的取值包含以下幾種:

TYPE:類,接口或者枚舉

FIELD:域,包含枚舉常量

METHOD:方法

PARAMETER:參數

CONSTRUCTOR:構造方法

LOCAL_VARIABLE:局部變量

ANNOTATION_TYPE:註解類型

PACKAGE:包

@Retention 註解
功能:指明修飾的註解的生存週期,即會保留到哪個階段。

RetentionPolicy的取值包含以下三種:

SOURCE:源碼級別保留,編譯後即丟棄。

CLASS:編譯級別保留,編譯後的class文件中存在,在jvm運行時丟棄,這是默認值。

RUNTIME: 運行級別保留,編譯後的class文件中存在,在jvm運行時保留,可以被反射調用。

@Documented 註解
功能:指明修飾的註解,可以被例如javadoc此類的工具文檔化,只負責標記,沒有成員取值。

@Inherited註解
功能:允許子類繼承父類中的註解。
使用

@Log(name = "getValue")

大部分內容都是來自這篇博客;
https://www.cnblogs.com/wangshen31/p/9379197.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章