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

  • 2019 年 10 月 17 日
  • 筆記

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/14