面向方面編程(AOP)經過提供另外一種思考程序結構的方式來補充面向對象編程(OOP)。
OOP中模塊化的關鍵單元是類,而在AOP中,模塊化單元是方面。php
首先,使用AOP要在build.gradle中加入依賴java
//引入AOP依賴
compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
複製代碼
而後在application.yml中加入mysql
spring:
aop:
proxy-target-class: true
複製代碼
定義一個切點
例如咱們要在一個方法加上切入點,根據方法的返回的對象,方法名,修飾詞來寫成一個表達式或者是具體的名字git
咱們如今來定義一個切點github
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() {
}
}
複製代碼
這裏的切點定義的方法是spring
@GetMapping("hello")
public String test(){
logger.info("歡迎關注Java知音");
return "i love java";
}
複製代碼
若是你想寫個切入點在全部返回對象爲Area的方法,以下
@Pointcut("execution(public com.example.entity.Area (..))")
等不少寫法,也能夠直接做用在某些包下
注意:private修飾的沒法攔截sql
在切入點開始處切入內容編程
在以前的AopTestController類中加入對test方法的前置通知瀏覽器
@Before("cutOffPoint()")
public void beforeTest(){
logger.info("我在test方法以前執行");
}
複製代碼
這裏@Before裏的值就是切入點所註解的方法名緩存
在方法左側出現的圖標跟過去之後就是所要通知的方法 這裏就是配置正確了,咱們來瀏覽器調用一下方法
聯想一下,這樣的效果能夠用在哪裏,想像若是要擴展一些代碼,在不須要動源代碼的基礎之上就能夠進行拓展,美滋滋
和前置通知相反,在切入點以後執行
@After("cutOffPoint()")
public void doAfter(){
logger.info("我是在test以後執行的");
}
複製代碼
控制檯執行結果
這裏定義一個通知須要重啓啓動類,而修改通知方法的內容是能夠熱部署的
和前兩個寫法不一樣,實現的效果包含了前置和後置通知
當使用環繞通知時,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.全局的事物處理 等
切入點返回結果以後執行,也就是都前置後置環繞都執行完了,這個就執行了
/** * 執行完請求能夠作的 * @param result * @throws Throwable */
@AfterReturning(returning = "result", pointcut = "cutOffPoint()")
public void doAfterReturning(Object result) throws Throwable {
logger.info("你們好,我是@AfterReturning,他們都秀完了,該我上場了");
}
複製代碼
執行結果
應用場景能夠用來在訂單支付完成以後就行二次的結果驗證,重要參數的二次校驗,防止在方法執行中的時候參數被修改等等
這個是在切入執行報錯的時候執行的
// 聲明錯誤e時指定的拋錯類型法必會拋出指定類型的異常
// 此處將e的類型聲明爲Throwable,對拋出的異常不加限制
@AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")
public void doAfterReturning(Throwable e) {
logger.info("你們好,我是@AfterThrowing,他們犯的錯誤,我來背鍋");
logger.info("錯誤信息"+e.getMessage());
}
複製代碼
在其餘切入內容中隨意整個錯誤出來,製造一個環境
下面是@AfterThrowing的執行結果
定義切入點攔截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切入處理,錯誤不用管,直接拋,拋到控制層進行處理,這樣的話,接口調用的時候,出錯了,接口不會什麼都不返回,而是會返回給你錯誤代碼,以及錯誤信息,便於開發人員查錯
先移除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中增長
#顯示mysql執行日誌
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地址:github.com/cuifuan