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

1.@Pointcut 切入點

定義一個切點。

例如我們要在一個方法加上切入點,根據方法的返回的對象,方法名,修飾詞來寫成一個表達式或者是具體的名字

我們現在來定義一個切點

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修飾的無法攔截

2.@Before前置通知

在切入點開始處切入內容

在之前的AopTestController類中加入對test方法的前置通知

@Before("cutOffPoint()")
    public void beforeTest(){
        logger.info("我在test方法之前執行");
    }

這里@Before里的值就是切入點所注解的方法名

Spring全家桶系列--SpringBoot之AOP詳解

在方法左側出現的圖標跟過去以后就是所要通知的方法 這里就是配置正確了,我們來瀏覽器調用一下方法

Spring全家桶系列--SpringBoot之AOP詳解

聯想一下,這樣的效果可以用在哪里,想像如果要擴展一些代碼,在不需要動源代碼的基礎之上就可以進行拓展,美滋滋

3.@After 后置通知

和前置通知相反,在切入點之后執行

@After("cutOffPoint()")
    public void doAfter(){
        logger.info("我是在test之后執行的");
    }

控制台執行結果

Spring全家桶系列--SpringBoot之AOP詳解

這里定義一個通知需要重啟啟動類,而修改通知方法的內容是可以熱部署的

4.@Around環繞通知

和前兩個寫法不同,實現的效果包含了前置和后置通知。

當使用環繞通知時,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;
    }

執行結果:

Spring全家桶系列--SpringBoot之AOP詳解

1.環繞通知可以項目做全局異常處理
2.日志記錄
3.用來做數據全局緩存
4.全局的事物處理 等

5.@AfterReturning

切入點返回結果之后執行,也就是都前置后置環繞都執行完了,這個就執行了

/**
     * 執行完請求可以做的
     * @param result
     * @throws Throwable
     */
    @AfterReturning(returning = "result", pointcut = "cutOffPoint()")
    public void doAfterReturning(Object result) throws Throwable {
        logger.info("大家好,我是@AfterReturning,他們都秀完了,該我上場了");
    }

執行結果

Spring全家桶系列--SpringBoot之AOP詳解

應用場景可以用來在訂單支付完成之后就行二次的結果驗證,重要參數的二次校驗,防止在方法執行中的時候參數被修改等等

6.@AfterThrowing

這個是在切入執行報錯的時候執行

// 聲明錯誤e時指定的拋錯類型法必會拋出指定類型的異常
    // 此處將e的類型聲明為Throwable,對拋出的異常不加限制
    @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")
    public void doAfterReturning(Throwable e) {
        logger.info("大家好,我是@AfterThrowing,他們犯的錯誤,我來背鍋");
        logger.info("錯誤信息"+e.getMessage());
    }

在其他切入內容中隨意整個錯誤出來,制造一個環境。

下面是@AfterThrowing的執行結果

Spring全家桶系列--SpringBoot之AOP詳解

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM