SpringBoot 配置 AOP 列印日誌

来源:https://www.cnblogs.com/vandusty/archive/2019/08/24/11406536.html
-Advertisement-
Play Games

在項目開發中,日誌系統是必不可少的,用`AOP`在Web的請求做入參和出參的參數列印,同時對異常進行日誌列印,避免重覆的手寫日誌,完整案例見文末源碼。 ...


在項目開發中,日誌系統是必不可少的,用AOP在Web的請求做入參和出參的參數列印,同時對異常進行日誌列印,避免重覆的手寫日誌,完整案例見文末源碼。

一、Spring AOP

AOP(Aspect-Oriented Programming,面向切麵編程),它利用一種"橫切"的技術,將那些多個類的共同行為封裝到一個可重用的模塊。便於減少系統的重覆代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。

AOP中有以下概念:

  • Aspect(切麵):聲明類似於Java中的類聲明,在Aspect中會包含一些Pointcut及相應的Advice。
  • Joint point(連接點):表示在程式中明確定義的點。包括方法的調用、對類成員的訪問等。
  • Pointcut(切入點):表示一個組Joint point,如方法名、參數類型、返回類型等等。
  • Advice(通知):Advice定義了在Pointcut裡面定義的程式點具體要做的操作,它通過(beforearoundafter(returnthrow)、finally來區別實在每個Joint point之前、之後還是執行 前後要調用的代碼。
  • Before:在執行方法前調用Advice,比如請求介面之前的登錄驗證。
  • Around:在執行方法前後調用Advice,這是最常用的方法。
  • After:在執行方法後調用Adviceafterreturn是方法正常返回後調用,after\throw是方法拋出異常後調用。
  • Finally:方法調用後執行Advice,無論是否拋出異常還是正常返回。
  • AOP proxyAOP proxy也是Java對象,是由AOP框架創建,用來完成上述動作,AOP對象通常可以通過JDK dynamic proxy完成,或者使用CGLIb完成。
  • Weaving:實現上述切麵編程的代碼織入,可以在編譯時刻,也可以在運行時刻,Spring和其它大多數Java框架都是在運行時刻生成代理。

二、項目示例

當然,在使用該案例之前,如果需要瞭解日誌配置相關,可參考 SpringBoot 非同步輸出 Logback 日誌, 本文就不再概述了。

2.1 在pom引入依賴

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- 分析客戶端信息的工具類-->
    <dependency>
        <groupId>eu.bitwalker</groupId>
        <artifactId>UserAgentUtils</artifactId>
        <version>1.20</version>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>1.8.4</scope>
    </dependency>
</dependencies>

2.2 Controller 切麵:WebLogAspect

@Aspect
@Component
@Slf4j
public class WebLogAspect {

    /**
     * 進入方法時間戳
     */
    private Long startTime;
    /**
     * 方法結束時間戳(計時)
     */
    private Long endTime;

    public WebLogAspect() {
    }


    /**
     * 定義請求日誌切入點,其切入點表達式有多種匹配方式,這裡是指定路徑
     */
    @Pointcut("execution(public * cn.van.log.aop.controller.*.*(..))")
    public void webLogPointcut() {
    }

    /**
     * 前置通知:
     * 1. 在執行目標方法之前執行,比如請求介面之前的登錄驗證;
     * 2. 在前置通知中設置請求日誌信息,如開始時間,請求參數,註解內容等
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLogPointcut()")
    public void doBefore(JoinPoint joinPoint) {

        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //獲取請求頭中的User-Agent
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        //列印請求的內容
        startTime = System.currentTimeMillis();
        log.info("請求開始時間:{}" + LocalDateTime.now());
        log.info("請求Url : {}" + request.getRequestURL().toString());
        log.info("請求方式 : {}" + request.getMethod());
        log.info("請求ip : {}" + request.getRemoteAddr());
        log.info("請求方法 : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("請求參數 : {}" + Arrays.toString(joinPoint.getArgs()));
        // 系統信息
        log.info("瀏覽器:{}", userAgent.getBrowser().toString());
        log.info("瀏覽器版本:{}", userAgent.getBrowserVersion());
        log.info("操作系統: {}", userAgent.getOperatingSystem().toString());
    }

    /**
     * 返回通知:
     * 1. 在目標方法正常結束之後執行
     * 1. 在返回通知中補充請求日誌信息,如返回時間,方法耗時,返回值,並且保存日誌信息
     *
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(returning = "ret", pointcut = "webLogPointcut()")
    public void doAfterReturning(Object ret) throws Throwable {
        endTime = System.currentTimeMillis();
        log.info("請求結束時間:{}" + LocalDateTime.now());
        log.info("請求耗時:{}" + (endTime - startTime));
        // 處理完請求,返回內容
        log.info("請求返回 : {}" + ret);
    }

    /**
     * 異常通知:
     * 1. 在目標方法非正常結束,發生異常或者拋出異常時執行
     * 1. 在異常通知中設置異常信息,並將其保存
     *
     * @param throwable
     */
    @AfterThrowing(value = "webLogPointcut()", throwing = "throwable")
    public void doAfterThrowing(Throwable throwable) {
        // 保存異常日誌記錄
        log.error("發生異常時間:{}" + LocalDateTime.now());
        log.error("拋出異常:{}" + throwable.getMessage());
    }
}

2.3 編寫測試

@RestController
@RequestMapping("/log")
public class LogbackController {

    /**
     * 測試正常請求
     * @param msg
     * @return
     */
    @GetMapping("/{msg}")
    public String getMsg(@PathVariable String msg) {
        return "request msg : " + msg;
    }

    /**
     * 測試拋異常
     * @return
     */
    @GetMapping("/test")
    public String getException(){
        // 故意造出一個異常
        Integer.parseInt("abc123");
        return "success";
    }
}

2.4 @Before@AfterReturning部分也可使用以下代碼替代

    /**
     * 在執行方法前後調用Advice,這是最常用的方法,相當於@Before和@AfterReturning全部做的事兒
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("webLogPointcut()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //獲取請求頭中的User-Agent
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        //列印請求的內容
        startTime = System.currentTimeMillis();
        log.info("請求Url : {}" , request.getRequestURL().toString());
        log.info("請求方式 : {}" , request.getMethod());
        log.info("請求ip : {}" , request.getRemoteAddr());
        log.info("請求方法 : " , pjp.getSignature().getDeclaringTypeName() , "." , pjp.getSignature().getName());
        log.info("請求參數 : {}" , Arrays.toString(pjp.getArgs()));
    // 系統信息
        log.info("瀏覽器:{}", userAgent.getBrowser().toString());
        log.info("瀏覽器版本:{}",userAgent.getBrowserVersion());
        log.info("操作系統: {}", userAgent.getOperatingSystem().toString());
        // pjp.proceed():當我們執行完切麵代碼之後,還有繼續處理業務相關的代碼。proceed()方法會繼續執行業務代碼,並且其返回值,就是業務處理完成之後的返回值。
        Object ret = pjp.proceed();
        log.info("請求結束時間:"+ LocalDateTime.now());
        log.info("請求耗時:{}" , (System.currentTimeMillis() - startTime));
        // 處理完請求,返回內容
        log.info("請求返回 : " , ret);
        return ret;
    }

三、 測試

3.1 請求入口LogbackController.java

@RestController
@RequestMapping("/log")
public class LogbackController {

    /**
     * 測試正常請求
     * @param msg
     * @return
     */
    @GetMapping("/normal/{msg}")
    public String getMsg(@PathVariable String msg) {
        return msg;
    }

    /**
     * 測試拋異常
     * @return
     */
    @GetMapping("/exception/{msg}")
    public String getException(@PathVariable String msg){
        // 故意造出一個異常
        Integer.parseInt("abc123");
        return msg;
    }
}

3.2 測試正常請求

打開瀏覽器,訪問http://localhost:8082/log/normal/hello

日誌列印如下:

[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [65] [INFO ] 請求開始時間:2019-02-24T22:37:50.892
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [66] [INFO ] 請求Url : http://localhost:8082/log/normal/hello
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [67] [INFO ] 請求方式 : GET
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [68] [INFO ] 請求ip : 0:0:0:0:0:0:0:1
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [69] [INFO ] 請求方法 : 
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [70] [INFO ] 請求參數 : [hello]
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [72] [INFO ] 瀏覽器:CHROME
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [73] [INFO ] 瀏覽器版本:76.0.3809.100
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [74] [INFO ] 操作系統: MAC_OS_X
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [88] [INFO ] 請求結束時間:2019-02-24T22:37:50.901
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [89] [INFO ] 請求耗時:14
[2019-02-24 22:37:50.050] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-1] [91] [INFO ] 請求返回 : hello

3.3 測試異常情況

訪問:http://localhost:8082/log/exception/hello

[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [65] [INFO ] 請求開始時間:2019-02-24T22:39:57.728
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [66] [INFO ] 請求Url : http://localhost:8082/log/exception/hello
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [67] [INFO ] 請求方式 : GET
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [68] [INFO ] 請求ip : 0:0:0:0:0:0:0:1
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [69] [INFO ] 請求方法 : 
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [70] [INFO ] 請求參數 : [hello]
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [72] [INFO ] 瀏覽器:CHROME
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [73] [INFO ] 瀏覽器版本:76.0.3809.100
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [74] [INFO ] 操作系統: MAC_OS_X
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [104] [ERROR] 發生異常時間:2019-02-24T22:39:57.731
[2019-02-24 22:39:57.057] [cn.van.log.aop.aspect.WebLogAspect] [http-nio-8082-exec-9] [105] [ERROR] 拋出異常:For input string: "abc123"
[2019-02-24 22:39:57.057] [org.apache.juli.logging.DirectJDKLog] [http-nio-8082-exec-9] [175] [ERROR] Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NumberFormatException: For input string: "abc123"] with root cause
java.lang.NumberFormatException: For input string: "abc123"

四、源碼

4.1 示例代碼

  1. Github 示例代碼

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、React的世界觀1、通過改變state來改變視圖視圖不用考慮如何改變自己,把state畫出來即可。2、變數不可變通過創建一個新的state來更改state,使得變更可追蹤,不容易因為其他部分修改state導致不可預測的錯誤3、結構與樣式分離參考了CSS的做法,RN的style機制使得代碼更清晰 ...
  • js中的數組和字元串有點類似,不是說本質上,而是在遍歷,獲取時的類似。從標識來說,可以一眼看出那個是數組,那個是字元串;但在使用遍歷時,會不經意的將兩者混淆,導致方法用錯。同時兩者的方法又有好幾個相同的,但需註意語義,以及有些方法是不會對原數組產生影響的。以下是我整理的一些關於數組和字元串的一些方法 ...
  • 介紹 在css2當中,存在標準模式下的盒子模型和IE下的怪異盒子模型。這兩種方案表示的是一種盒子模型的渲染模式。而在css3當中,新增加了彈性盒子模型,彈性盒子模型是一種新增加的強大的、靈活的佈局方案。彈性盒子模型是css3中新提出的一種佈局方案。是一種為了應對針對不同屏幕寬度不同設備的一整套新的布 ...
  • 一、什麼是 iframe iframe 用於在頁面內顯示頁面,使用 <iframe> 會創建包含另外一個文檔的內聯框架(即行內框架) 二、iframe 的常用屬性 1、width 定義 iframe 的寬度 2、height 定義 iframe 的高度 3、name 規定 iframe 的名稱 4、 ...
  • HTML5/CSS簡介 首先來說一說什麼是HTML5,HTML5可以認為是字面上的意義,也就是HTML的第五代產品,當然從另一個角度來說它是一種新的富客戶端解決方案。 HTML5 將成為 HTML、XHTML 以及 HTML DOM 的新標準。 HTML 的上一個版本誕生於 1999 年。自從那以後 ...
  • 狀態模式 允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。 簡單的解釋一下: 第一部分的意思是將狀態封裝成獨立的類,並將請求委托給當前的狀態對象,當對象的內部狀態改變時,會帶來不同的行為變化。 第二部分是從客戶的角度來看,我們使用的對象,在不同的狀態下具有截然不同的行為,這個 ...
  • 摘要: 玩轉ES6解構賦值。 原文: "5個 JS 解構有趣的用途" 譯者:前端小智 1. 交換變數 通常交換兩個變數的方法需要一個額外的臨時變數,來看看例子: 是一個臨時變數,它先保存 的值。然後把 的值賦值給 ,接著將 值賦給 。 如果使用解構的方式會更簡單,不需要什麼鬼的 變數。 是解構賦值, ...
  • 是否有一些函數可以告訴我字元串在記憶體中占用多少位元組? 我需要設置套接字緩衝區的大小,以便一次傳輸整個字元串。 解決方案 但實際上你需要知道它代表的長度,所以類似的東西len(s)應該足夠了。 本文首發於Python黑洞網,博客園同步更新 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...