Spring全家桶系列–SpringBoot之AOP詳解
- //本文作者:cuifuan
- //本文將收錄到選單欄:《Spring全家桶》專欄中
面向方面程式設計(AOP)通過提供另一種思考程式結構的方式來補充面向物件程式設計(OOP)。
OOP中模組化的關鍵單元是類,而在AOP中,模組化單元是方面。
準備工作
首先,使用AOP要在build.gradle中加入依賴
//引入AOP依賴 compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
然後在application.yml中加入
spring: aop: proxy-target-class: true
[email protected] 切入點
定義一個切點。
例如我們要在一個方法加上切入點,根據方法的返回的物件,方法名,修飾詞來寫成一個表示式或者是具體的名字
我們現在來定義一個切點
package com.example.aop; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component; /** * 類定義為切面類 */ @Aspect @Component public class AopTestController { private static final Logger logger = LoggerFactory.getLogger(AopTestController.class); /** * 定義一個切點 */ @Pointcut(value = "execution(public String test (..))")public void cutOffPoint() { } }
這裡的切點定義的方法是
@GetMapping("hello") public String test(){ logger.info("歡迎關注Java知音"); return "i love java"; }
如果你想寫個切入點在所有返回物件為Area的方法,如下
@Pointcut("execution(public com.example.entity.Area (..))")
等很多寫法,也可以直接作用在某些包下
注意:private修飾的無法攔截
[email protected]前置通知
在切入點開始處切入內容
在之前的AopTestController類中加入對test方法的前置通知
@Before("cutOffPoint()") public void beforeTest(){ logger.info("我在test方法之前執行"); }
這裡@Before裡的值就是切入點所註解的方法名
在方法左側出現的圖示跟過去以後就是所要通知的方法 這裡就是配置正確了,我們來瀏覽器呼叫一下方法
聯想一下,這樣的效果可以用在哪裡,想像如果要擴充套件一些程式碼,在不需要動原始碼的基礎之上就可以進行拓展,美滋滋
[email protected] 後置通知
和前置通知相反,在切入點之後執行
@After("cutOffPoint()") public void doAfter(){ logger.info("我是在test之後執行的"); }
控制檯執行結果
這裡定義一個通知需要重啟啟動類,而修改通知方法的內容是可以熱部署的
[email protected]環繞通知
和前兩個寫法不同,實現的效果包含了前置和後置通知。
當使用環繞通知時,proceed方法必須呼叫,否則攔截到的方法就不會再執行了
環繞通知=前置+目標方法執行+後置通知,proceed方法就是用於啟動目標方法執行的
ThreadLocal<Long> startTime = new ThreadLocal<>(); @Around("cutOffPoint()") public Object doAround(ProceedingJoinPoint pjp){ startTime.set(System.currentTimeMillis()); logger.info("我是環繞通知執行"); Object obj; try{ obj = pjp.proceed(); logger.info("執行返回值 : " + obj); logger.info(pjp.getSignature().getName()+"方法執行耗時: " + (System.currentTimeMillis() - startTime.get())); } catch (Throwable throwable) { obj=throwable.toString(); } return obj; }
執行結果:
1.環繞通知可以專案做全域性異常處理
2.日誌記錄
3.用來做資料全域性快取
4.全域性的事物處理 等
[email protected]
切入點返回結果之後執行,也就是都前置後置環繞都執行完了,這個就執行了
/** * 執行完請求可以做的 * @param result * @throws Throwable */ @AfterReturning(returning = "result", pointcut = "cutOffPoint()") public void doAfterReturning(Object result) throws Throwable { logger.info("大家好,我是@AfterReturning,他們都秀完了,該我上場了"); }
執行結果
應用場景可以用來在訂單支付完成之後就行二次的結果驗證,重要引數的二次校驗,防止在方法執行中的時候引數被修改等等
[email protected]
這個是在切入執行報錯的時候執行
// 宣告錯誤e時指定的拋錯型別法必會丟擲指定型別的異常 // 此處將e的型別宣告為Throwable,對丟擲的異常不加限制 @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()") public void doAfterReturning(Throwable e) { logger.info("大家好,我是@AfterThrowing,他們犯的錯誤,我來背鍋"); logger.info("錯誤資訊"+e.getMessage()); }
在其他切入內容中隨意整個錯誤出來,製造一個環境。
下面是@AfterThrowing的執行結果
7.AOP用在全域性異常處理
定義切入點攔截ResultBean或者PageResultBean
@Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))") public void handlerPageResultBeanMethod() { } @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))") public void handlerResultBeanMethod() { }
下面是AopController.java
package com.example.aop; import com.example.beans.PageResultBean; import com.example.beans.ResultBean; import com.example.entity.UnloginException; import com.example.exception.CheckException; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * 使用@Aspect註解將此類定義為切面類 * 根據曉風輕著的ControllerAOP所修改 * 曉風輕大佬(很大的佬哥了):https://xwjie.github.io/ */ @Aspect @Component public class AopController { private static final Logger logger = LoggerFactory.getLogger(AopController.class); ThreadLocal<ResultBean> resultBeanThreadLocal = new ThreadLocal<>(); ThreadLocal<PageResultBean<?>> pageResultBeanThreadLocal = new ThreadLocal<>(); ThreadLocal<Long> start = new ThreadLocal<>(); /** * 定義一個切點 */ @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))") public void handlerPageResultBeanMethod() { } @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))") public void handlerResultBeanMethod() { } @Around("handlerPageResultBeanMethod()") public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) { start.set(System.currentTimeMillis()); try { pageResultBeanThreadLocal.set((PageResultBean<?>)pjp.proceed()); logger.info(pjp.getSignature() + " 方法執行耗時:" + (System.currentTimeMillis() - start.get())); } catch (Throwable e) { ResultBean<?> resultBean = handlerException(pjp , e); pageResultBeanThreadLocal.set(new PageResultBean<>().setMsg(resultBean.getMsg()).setCode(resultBean.getCode())); } return pageResultBeanThreadLocal.get(); } @Around("handlerResultBeanMethod()") public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) { start.set(System.currentTimeMillis()); try { resultBeanThreadLocal.set((ResultBean<?>)pjp.proceed()); logger.info(pjp.getSignature() + " 方法執行耗時:" + (System.currentTimeMillis() - start.get())); } catch (Throwable e) { resultBeanThreadLocal.set(handlerException(pjp , e)); } return resultBeanThreadLocal.get(); } /** * 封裝異常資訊,注意區分已知異常(自己丟擲的)和未知異常 */ private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) { ResultBean<?> result = new PageResultBean(); logger.error(pjp.getSignature() + " error ", e); // 已知異常 if (e instanceof CheckException) { result.setMsg(e.getLocalizedMessage()); result.setCode(ResultBean.FAIL); } else if (e instanceof UnloginException) { result.setMsg("Unlogin"); result.setCode(ResultBean.NO_LOGIN); } else { result.setMsg(e.toString()); result.setCode(ResultBean.FAIL); } return result; } }
用上面的環繞通知可以對所有返回ResultBean或者PageResultBean的方法進行切入,這樣子就不用在業務層去捕捉錯誤了,只需要去列印自己的info日誌。
看下面一段程式碼
@Transactional @Override public int insertSelective(Area record) { record.setAddress("test"); record.setPostalcode(88888); record.setType(3); int i=0; try { i = areaMapper.insertSelective(record); }catch (Exception e){ logger.error("AreaServiceImpl insertSelective error:"+e.getMessage()); } return i; }
假如上面的插入操作失敗出錯了? 你認為會回滾嗎?
答案是:不會。
為什麼?
因為你把錯誤捕捉了,事物沒檢測到異常就不會回滾。
那麼怎麼才能回滾呢?
在catch里加throw new RuntimeException().
可是那麼多業務方法每個設計修改的操作都加,程式碼繁瑣,怎麼進行處理呢?
在這裡用到上面的AOP切入處理,錯誤不用管,直接拋,拋到控制層進行處理,這樣的話,介面呼叫的時候,出錯了,介面不會什麼都不返回,而是會返回給你錯誤程式碼,以及錯誤資訊,便於開發人員查錯。
8.以上用的是log4j2的日誌處理
先移除springboot自帶的log日誌處理
在build.gradle中增加
configurations { providedRuntime // 去除SpringBoot自帶的日誌 all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' } ext { springBootVersion = '2.0.1.RELEASE' } dependencies { compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}" }
然後在application.yml中增加
logging:
level:
com:
example:
dao: debug
config: classpath:log4j2-spring.xml
log4j2-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <!--日誌級別以及優先順序排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--Configuration後面的status,這個用於設定log4j2自身內部的資訊輸出,可以不設定,當設定成trace時,你會看到log4j2內部各種詳細輸出--> <!--monitorInterval:Log4j能夠自動檢測修改配置 檔案和重新配置本身,設定間隔秒數--> <configuration status="INFO" monitorInterval="30"> <!--先定義所有的appender--> <appenders> <!--這個輸出控制檯的配置--> <console name="Console" target="SYSTEM_OUT"> <!--輸出日誌的格式--> <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/> </console> <!--檔案會打印出所有資訊,這個log每次執行程式會自動清空,由append屬性決定,這個也挺有用的,適合臨時測試用--> <File name="Test" fileName="logs/test.log" append="false"> <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/> </File> <RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}"> <!-- 只接受level=INFO以上的日誌 --> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/> <Policies> <TimeBasedTriggeringPolicy modulate="true" interval="1"/> <SizeBasedTriggeringPolicy/> </Policies> </RollingFile> <RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}"> <!-- 只接受level=WARN以上的日誌 --> <Filters> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" /> </Filters> <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/> <Policies> <TimeBasedTriggeringPolicy modulate="true" interval="1"/> <SizeBasedTriggeringPolicy/> </Policies> </RollingFile> </appenders> <!--然後定義logger,只有定義了logger並引入的appender,appender才會生效--> <loggers> <!--過濾掉spring和mybatis的一些無用的DEBUG資訊--> <logger name="org.springframework" level="INFO"></logger> <logger name="org.mybatis" level="INFO"></logger> <root level="all"> <appender-ref ref="Console"/> <appender-ref ref="Test"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileError"/> </root> </loggers> </configuration>
之後在你要列印日誌的類中增加
private static final Logger logger = LoggerFactory.getLogger(你的類名.class); public static void main(String[] args) { logger.error("error級別日誌"); logger.warn("warning級別日誌"); logger.info("info級別日誌"); }
有了日誌後就很方便了,在你的方法接收物件時列印下,然後執行了邏輯之後列印下, 出錯之後很明確了,就會很少去Debug的,養成多打日誌的好習慣,多列印一點info級別的日誌,用來在開發環境使用,在上線的時候把列印的最低級別設定為warning,這樣你的info級別日誌也不會影響到專案的重要Bug的列印
寫這個部落格的時候我也在同時跑著這個專案,有時候會出現一些錯誤,例如jar包版本,業務層引用無效,AOP設定不生效等等,也同時在排查解決,如果你遇到了同樣的錯誤,可以去我的GitHub聯絡我,如小弟有時間或許也能幫到你,謝謝
Github地址:https://github.com/cuifuan