1. 程式人生 > >Spring Boot 2.X(八):Spring AOP 實現簡單的日誌切面

Spring Boot 2.X(八):Spring AOP 實現簡單的日誌切面

AOP

1.什麼是 AOP ?

AOP 的全稱為 Aspect Oriented Programming,譯為面向切面程式設計,是通過預編譯方式和執行期動態代理實現核心業務邏輯之外的橫切行為的統一維護的一種技術。AOP 是面向物件程式設計(OOP)的補充和擴充套件。
利用 AOP 可以對業務邏輯各部分進行隔離,從而達到降低模組之間的耦合度,並將那些影響多個類的公共行為封裝到一個可重用模組,從而到達提高程式的複用性,同時提高了開發效率,提高了系統的可操作性和可維護性。

2.為什麼要用 AOP ?

在實際的 Web 專案開發中,我們常常需要對各個層面實現日誌記錄,效能統計,安全控制,事務處理,異常處理等等功能。如果我們對每個層面的每個類都獨立編寫這部分程式碼,那久而久之程式碼將變得很難維護,所以我們把這些功能從業務邏輯程式碼中分離出來,聚合在一起維護,而且我們能靈活地選擇何處需要使用這些程式碼。

3.AOP 的核心概念

名詞 概念 理解
通知(Advice) 攔截到連線點之後所要執行的程式碼,通知分為前置、後置、異常、最終、環繞通知五類 我們要實現的功能,如日誌記錄,效能統計,安全控制,事務處理,異常處理等等,說明什麼時候要幹什麼
連線點(Joint Point) 被攔截到的點,如被攔截的方法、對類成員的訪問以及異常處理程式塊的執行等等,自身還能巢狀其他的 Joint Point Spring 允許你用通知的地方,方法有關的前前後後(包括丟擲異常)
切入點(Pointcut) 對連線點進行攔截的定義 指定通知到哪個方法,說明在哪幹
切面(Aspect) 切面類的定義,裡面包含了切入點(Pointcut)和通知(Advice)的定義 切面就是通知和切入點的結合
目標物件(Target Object) 切入點選擇的物件,也就是需要被通知的物件;由於 Spring AOP 通過代理模式實現,所以該物件永遠是被代理物件 業務邏輯本身
織入(Weaving) 把切面應用到目標物件從而創建出 AOP 代理物件的過程。織入可以在編譯期、類裝載期、執行期進行,而 Spring 採用在執行期完成 切點定義了哪些連線點會得到通知
引入(Introduction ) 可以在執行期為類動態新增方法和欄位,Spring 允許引入新的介面到所有目標物件 引入就是在一個介面/類的基礎上引入新的介面增強功能
AOP 代理(AOP Proxy ) Spring AOP 可以使用 JDK 動態代理或者 CGLIB 代理,前者基於介面,後者基於類 通過代理來對目標物件應用切面

Spring AOP

1.簡介

AOP 是 Spring 框架中的一個核心內容。在 Spring 中,AOP 代理可以用 JDK 動態代理或者 CGLIB 代理 CglibAopProxy 實現。Spring 中 AOP 代理由 Spring 的 IOC 容器負責生成和管理,其依賴關係也由 IOC 容器負責管理。

2.相關注解

註解 說明
@Aspect 將一個 java 類定義為切面類
@Pointcut 定義一個切入點,可以是一個規則表示式,比如下例中某個 package 下的所有函式,也可以是一個註解等
@Before 在切入點開始處切入內容
@After 在切入點結尾處切入內容
@AfterReturning 在切入點 return 內容之後處理邏輯
@Around 在切入點前後切入內容,並自己控制何時執行切入點自身的內容
@AfterThrowing 用來處理當切入內容部分丟擲異常之後的處理邏輯
@Order(100) AOP 切面執行順序, @Before 數值越小越先執行,@After 和 @AfterReturning 數值越大越先執行

其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都屬於通知(Advice)。

利用 AOP 實現 Web 日誌處理

1.構建專案

2.新增依賴

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 熱部署模組 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 -->
        </dependency>
        <!-- Spring AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

3.Web 日誌註解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ControllerWebLog {
     String name();//所呼叫介面的名稱
     boolean intoDb() default false;//標識該條操作日誌是否需要持久化儲存
}

4.實現切面邏輯

@Aspect
@Component
@Order(100)
public class WebLogAspect {

    private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    private ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<Map<String, Object>>();

    /**
     * 橫切點
     */
    @Pointcut("execution(public * cn.zwqh.springboot.controller..*.*(..))")
    public void webLog() {
    }
    /**
     * 接收請求,並記錄資料
     * @param joinPoint
     * @param controllerWebLog
     */
    @Before(value = "webLog()&& @annotation(controllerWebLog)")
    public void doBefore(JoinPoint joinPoint, ControllerWebLog controllerWebLog) {
        // 接收到請求
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();
        // 記錄請求內容,threadInfo儲存所有內容
        Map<String, Object> threadInfo = new HashMap<>();
        logger.info("URL : " + request.getRequestURL());
        threadInfo.put("url", request.getRequestURL());
        logger.info("URI : " + request.getRequestURI());
        threadInfo.put("uri", request.getRequestURI());
        logger.info("HTTP_METHOD : " + request.getMethod());
        threadInfo.put("httpMethod", request.getMethod());
        logger.info("REMOTE_ADDR : " + request.getRemoteAddr());
        threadInfo.put("ip", request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "."
                + joinPoint.getSignature().getName());
        threadInfo.put("classMethod",
                joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
        threadInfo.put("args", Arrays.toString(joinPoint.getArgs()));
        logger.info("USER_AGENT"+request.getHeader("User-Agent"));
        threadInfo.put("userAgent", request.getHeader("User-Agent"));
        logger.info("執行方法:" + controllerWebLog.name());
        threadInfo.put("methodName", controllerWebLog.name());
        threadLocal.set(threadInfo);
    }
    /**
     * 執行成功後處理
     * @param controllerWebLog
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(value = "webLog()&& @annotation(controllerWebLog)", returning = "ret")
    public void doAfterReturning(ControllerWebLog controllerWebLog, Object ret) throws Throwable {
        Map<String, Object> threadInfo = threadLocal.get();
        threadInfo.put("result", ret);
        if (controllerWebLog.intoDb()) {
            //插入資料庫操作
            //insertResult(threadInfo);
        }
        // 處理完請求,返回內容
        logger.info("RESPONSE : " + ret);
    }
    /**
     * 獲取執行時間
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object ob = proceedingJoinPoint.proceed();
        Map<String, Object> threadInfo = threadLocal.get();
        Long takeTime = System.currentTimeMillis() - startTime;
        threadInfo.put("takeTime", takeTime);
        logger.info("耗時:" + takeTime);
        threadLocal.set(threadInfo);
        return ob;
    }
    /**
     * 異常處理
     * @param throwable
     */
    @AfterThrowing(value = "webLog()", throwing = "throwable")
    public void doAfterThrowing(Throwable throwable) {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();

        ServletRequestAttributes sra = (ServletRequestAttributes) ra;

        HttpServletRequest request = sra.getRequest();
        // 異常資訊
        logger.error("{}介面呼叫異常,異常資訊{}", request.getRequestURI(), throwable);
    }

}

5.測試介面

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/getOne")
    @ControllerWebLog(name = "查詢", intoDb = true)
    public String getOne(Long id, String name) {

        return "1234";
    }
}

6.執行測試

瀏覽器請求:http://127.0.0.1:8080/user/getOne?id=1&name=zwqh ,可以看到後臺日誌輸出:

小結

日誌記錄只是一個簡單的示例,而 Spring AOP 的應用讓整個系統變的更加有條不紊,在其他場景應用也很強大。
它幫助我們降低模組間耦合度,提高程式複用性,提高開發效率,提高系統可做性和可維護性。

示例程式碼

github

碼雲

非特殊說明,本文版權歸 朝霧輕寒 所有,轉載請註明出處.

原文標題:Spring Boot 2.X(八):Spring AOP 實現簡單的日誌切面

原文地址:https://www.zwqh.top/article/info