spring boot slf4j logback listener filter json aop http get scheduling tasks

Spring Boot EP 12:透過AOP將重複的程式整合,並在特定時機執行

Aspect Oriented Programming, AOP

在撰寫程時,很多時候都會有一些重複發生的需求,例如紀錄日誌、發送訊息等,一般而言會在任何有需求的地方撰寫程式,但這樣對後續的維護並不友善,而且容易改A壞B,或是掛萬漏一。既然這些重複的需求都在做差不多的事情,也在差不多的時候要執行,那不如就將他們抽離出來。AOP就是將這些重複的事情整合起來在,並在規定的時間執行。

AOP要先定義一個Pointcut (切入點),姑且把Pointcut視作某一段程式的代言人,該段程式可以是一整個類別或是一個函數,根據Pointcut有幾個重要的程式執行時間,如下:

  • Before:在Pointcut被執行之前。
  • AfterReturning:在Pointcut正常完成之後。
  • AfterThrowing:在Pointcut出現錯誤(Exception)之後。
  • After:無論Pointcut有無正常完成都要執行。

實作AOP

步驟1:在”STOCKMARKET/src/main/java/stockmarket/jovepater/com/stockmarket/”建立一個名為Aspects的資料夾,並新增一個LoggingAspect.java檔案。

步驟2:實作AOP,如下:

package stockmarket.jovepater.com.stockmarket.Aspects;

import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
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.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
// 可以有多的AOP,並透過@Order()來控制執行順序
@Order(0)
public class LoggingAspect {

    // 切入點,定義Controller下的所有程式都被涵蓋在內
    @Pointcut("execution(public * stockmarket.jovepater.com.stockmarket.Controllers..*.*(..))")
    public void LoggingAspectPointcut() {
        // 基本上不用特別實作
    }

    // 宣告時間戳記使用的變數,紀錄Controller的執行起訖
    Long longTimeStampStart, longTimeStampEnd;

    // @Before表示在Controller之前運作
    @Before("LoggingAspectPointcut()")
    public void BeforePointcut(JoinPoint joinPoint) {
        // 紀錄Controller啟動時間
        longTimeStampStart = System.currentTimeMillis();
    }

    // 帶入JoinPoint表示被執行的那個Pointcut程式類別
    // @AfterReturning表示在Controller正常完成之後運作
    @AfterReturning("LoggingAspectPointcut()")
    public void AfterReturningOfPointcut(JoinPoint joinPoint) {
        // 在日誌輸出Controller正常執行完成的資訊
        Logger(LoggerFactory.getLogger(joinPoint.getTarget().getClass()), "API Finished Normally.");
    }

    // @AfterThrowing表示Controller發生錯誤時要做的事情
    @AfterThrowing(value = "LoggingAspectPointcut()", throwing = "exception")
    public void AfterThrowingOfPointcut(JoinPoint joinPoint, Exception exception) {
        Logger myLogger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());

        // 在日誌輸出Exception資訊
        myLogger.error("API Finished by Exception: {}", exception.getMessage());
    }

    // @After表示Controller無論有沒有正常結束都要做的事情
    @After("LoggingAspectPointcut()")
    public void AfterPointcut(JoinPoint joinPoint) {
        // 紀錄Controller結束時間
        longTimeStampEnd = System.currentTimeMillis();

        // 起訖時間相減取得該Controller的執行時間
        Long longTimeDiff = longTimeStampEnd - longTimeStampStart;

        // 在日誌輸出Controller執行時間
        Logger(LoggerFactory.getLogger(joinPoint.getTarget().getClass()),
                "Time used: " + Objects.toString(longTimeDiff) + " ms");

        // 在日誌輸出Request相關資訊
        Logger(LoggerFactory.getLogger(joinPoint.getTarget().getClass()), joinPoint);
    }

    // 注入HttpServletRequest以便取得Request資訊
    @Autowired
    private HttpServletRequest request;

    // 注入HttpServletResponse以便取得Response資訊
    @Autowired
    private HttpServletResponse response;

    private void Logger(Logger myLogger, JoinPoint joinPoint) {
        String protocol = Objects.toString(request.getProtocol(), "");
        String method = request.getMethod();
        String scheme = request.getScheme();
        String uri = request.getRequestURI();
        String agent = request.getHeader("user-agent");
        String remoteaddr = request.getRemoteAddr();
        int remoteaport = request.getRemotePort();
        String localaddr = request.getLocalAddr();
        int localport = request.getLocalPort();

        String sessionid = request.getSession().getId();

        String classname = joinPoint.getTarget().getClass().getName();
        String classmethod = joinPoint.getSignature().getName();

        String strMessage = "\r\nController Logging Message:\r\n";
        strMessage += String.format("%s\r\n", "---------------------------------------------------");
        strMessage += String.format("  Session ID: %s\r\n", sessionid);
        strMessage += String.format("    Protocol: %s\r\n", protocol);
        strMessage += String.format("      Method: %s\r\n", method);
        strMessage += String.format("         Url: %s://%s:%d%s\r\n", scheme, localaddr, localport, uri);
        strMessage += String.format("       Agent: %s\r\n", agent);
        strMessage += String.format("      Remote: %s:%d\r\n", remoteaddr, remoteaport);
        strMessage += String.format("  Class Name: %s\r\n", classname);
        strMessage += String.format("Class Method: %s", classmethod);

        myLogger.info(strMessage);
    }

    private void Logger(Logger myLogger, String message) {
        myLogger.info(message);
    }
}

步驟3:當任何一個Restful API被呼叫,並且沒有發出任何錯誤,AOP會統一處理日誌與執行時間的計算。

日誌訊息如下:

2022-02-04 19:37:34.757 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/stocks , Msg: Auth OK.
2022-02-04 19:37:34.757 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/stocks , Msg: Content Type Checked.
2022-02-04 19:37:34.757 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/stocks , from: 127.0.0.1
# Controller正常結束,觸發AfterReturning將資訊顯示在日誌中
2022-02-04 19:37:34.916 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Controllers.StockController: API Finished Normally.
# 顯示這個Controller從啟動到執行完成所花費的時間
2022-02-04 19:37:34.916 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Controllers.StockController: Time used: 130 ms
# 將Controller的Request資訊顯示在日誌中
2022-02-04 19:37:34.947 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Controllers.StockController: 
Controller Logging Message:
---------------------------------------------------
  Session ID: 6071ECEA2AB595F42F7283F5B80C65C8
    Protocol: HTTP/1.1
      Method: GET
         Url: http://127.0.0.1:8080/api/stocks
       Agent: PostmanRuntime/7.28.4
      Remote: 127.0.0.1:61349
  Class Name: stockmarket.jovepater.com.stockmarket.Controllers.StockController
Class Method: getAllMarkets
2022-02-04 19:37:35.019 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Response: 200 /api/stocks

步驟4:在此,在Hello這個API加入會出現錯誤的程式碼(int x = 5 / 0;)。

@RestController
public class HelloController {

    @GetMapping("hello")
    public RspBody Hello() {
        // 宣告SLF4J的Logger日誌
        Logger myLogger = LoggerFactory.getLogger(HelloController.class);
        // 紀錄各種等級的事件
        myLogger.trace("This is Hello API.");
        myLogger.debug("This is Hello API.");
        myLogger.info("This is Hello API.");
        myLogger.warn("This is Hello API.");
        myLogger.error("This is Hello API.");

        // 不允許除以0,程式會出現錯誤
        int x = 5 / 0;

        return new RspBody("0000", "Success", "Hello World");
    }
}

步驟5:執行上述Restful API會出現錯誤,AOP也會透過”AfterThrowing”執行相對應的動作。

日誌訊息如下:

2022-02-04 19:42:09.494 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/hello , Msg: Auth OK.
2022-02-04 19:42:09.494 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/hello , Msg: Content Type Checked.
2022-02-04 19:42:09.494 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/hello , from: 127.0.0.1
2022-02-04 19:42:09.500 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
2022-02-04 19:42:09.500 WARN  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
2022-02-04 19:42:09.500 ERROR [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
# 觸發AfterThrowing,並將Exception資訊透過顯示在日誌中
2022-02-04 19:42:09.500 ERROR [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: API Finished by Exception: / by zero
# 顯示這個Controller從啟動到因錯誤停止所花費的時間
2022-02-04 19:42:09.500 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: Time used: 5 ms
2022-02-04 19:42:09.501 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: 
Controller Logging Message:
---------------------------------------------------
  Session ID: 6071ECEA2AB595F42F7283F5B80C65C8
    Protocol: HTTP/1.1
      Method: GET
         Url: http://127.0.0.1:8080/api/hello
       Agent: PostmanRuntime/7.28.4
      Remote: 127.0.0.1:61357
  Class Name: stockmarket.jovepater.com.stockmarket.Controllers.HelloController
Class Method: Hello
# 仍會發出原本的Exception資訊,AOP並不會取代原本的機制
2022-02-04 19:42:09.509 ERROR [http-nio-8080-exec-3] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/api].[dispatcherServlet]: Servlet.service() for servlet [dispatcherServlet] in context with path [/api] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
        at stockmarket.jovepater.com.stockmarket.Controllers.HelloController.Hello(HelloController.java:23)
        at stockmarket.jovepater.com.stockmarket.Controllers.HelloController$$FastClassBySpringCGLIB$$cb294f00.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
...
...
...

~ END ~


, ,

Related posts

Latest posts