基於 AOP 抽離方法的重複代碼

背景

今天師兄和我說,「之葉,你設計一個方案,把目前業務方法中和業務無關的邏輯都抽離出來,讓每一個方法只關心本身的業務邏輯」。我會心一笑 👇(由於咱們早應該作這件事情了)java

邪魅一笑

現有的業務方法

以前代碼裏每一個業務方法幾乎都是長這樣:git

public class XxxServiceImpl implements XxxService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public XxxResponse<...> queryXxx(XxxRequest request) {
        // 記錄方法開始時間
        long startTime = System.currentTimeMillis();
        // 構造響應
        XxxResponse<PagedData> response = new XxxResponse();
        // 設置調用機器
        response.setHost(ServiceUtils.getHost());
        // 設置方法開始執行時間
        response.setSysTime(startTime);

        try {
            // 業務邏輯代碼
            ......

            response.setData(pagedData);
        } catch(Throwable e) {
            // 拋出異常時候執行
            logger.error(...);
            response.failBizInfo(ServiceBizError.UNKNOWN_ERROR);
        } finally {
            // 設置方法耗時
            long costTime = System.currentTimeMillis() - startTime;
            response.setCostTime(costTime);
            // 記錄調用信息
            logger.info(...);
        }
        // 返回響應
        return response;
    }
  
      // 後面還有若干個相似的業務方法
      ......
}

很容易能夠看出,記錄方法開始時間捕獲異常並處理打印錯誤日誌記錄方法耗時 這些都是和業務沒有關係的,業務方法關心的,只應該是 業務邏輯代碼 纔對。一兩個方法這個樣子看起來也還好,可是目前項目裏面已經有十幾個這種樣子的方法了,並且之後還會更多,重複代碼對咱們簡直不能忍 —— 是的,我也早就看這些業務方法不順眼了,安排!github

必須安排

設計方案

AOP 登場

你們都聽過 Spring 有兩大神器 —— IoC 和 AOP —— 瞭解 AOP 的人,都知道 AOP 是 Aspect Oriented Programming,即面向切面編程:經過預編譯方式(CGLib)或者運行期動態代理(JDK Proxy)來實現程序功能代理的技術。此時的狀況,就完美匹配 AOP 的應用場景。咱們能夠定義一個切點(PointCut,也叫鏈接點),而後對和 切點匹配的方法,織入(Weaving)切面(Aspect),進行加強(Advice)處理:即在方法 調用前調用後 或者 拋出異常時,進行額外的處理。編程

實現方案

搭建示例項目

爲了方便示例,首先咱們創建一個簡單的 SpringBoot 項目,並添加示例的 Service 和 Controller:緩存

建立 SpringBoot 項目

加入一個 DemoService:安全

public interface DemoService {

    /**
     * 除法運算
     *
     * @param request 除法運算請求
     * @return 除法運算結果
     */
    DivisionResponse divide(DivisionRequest request);

}

DemoService 的實現:服務器

@Service
public class DemoServiceImpl implements DemoService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public DivisionResponse divide(DivisionRequest request) {
        long startTime = System.currentTimeMillis();

        DivisionResponse response = new DivisionResponse();
        // 設置方法調用的時間
        response.setSysTime(startTime);
        // 設置方法調用的機器
        response.setHost(getHost());

        // 請求參數
        int dividend = request.getDividend();
        int divisor = request.getDivisor();

        try {
            // 模擬檢查業務參數
            // ...檢查業務參數...
            TimeUnit.MILLISECONDS.sleep(300);

            // 模擬執行業務
            int result = dividend / divisor;

            // 設置業務執行結果
            response.setData(result);
            // 調用正常
            response.setSuccess(true);
        } catch (Throwable e) {
            // 調用出錯
            response.setSuccess(false);
            // 記錄執行錯誤
            logger.error("DemoServiceImpl.divide 執行出錯", e);
            response.setPrompt(e.getMessage());
        } finally {
            // 設置方法調用耗時
            response.setCostTime(System.currentTimeMillis() - startTime);
            // 記錄方法調用信息
            logger.info("DemoServiceImpl.divide request={}, response={}", request, response);
        }

        return response;
    }

    /**
     * 模擬得到服務器名稱
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }
}

再加入一個 DemoController:app

@RestController
public class DemoController {

    @Resource
    private DemoService demoService;

    @GetMapping("division.do")
    public DivisionResponse doDivision(@RequestParam int a,
                                       @RequestParam int b) {
        // 構建請求
        DivisionRequest request = new DivisionRequest();
        request.setDividend(a);
        request.setDivisor(b);

        // 執行
        return demoService.divide(request);
    }
}

啓動應用,看一下目前調用業務方法時的狀況:dom

  1. 調用正常狀況(a=2,b=1)

    正常狀況

  2. 調用出錯狀況(a=2,b=0)

    錯誤狀況

編寫切面

如今的 Java Web 應用,使用註解來進行配置和作 AOP 已是主流 —— 由於相比 XML,註解更簡單並且更好用。因此咱們先定義一個 @ServiceMethodAspectAnnoide

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceMethodAspectAnno {
    
}

這個註解的目標類型是 方法,而且在 運行期 保留。而後咱們就能夠來定義切面了,這個切面會攔截全部被 @ServiceMethodAspectAnno 註解的方法,並作織入處理:

@Component
@Aspect  // @Aspect 告訴 Spring 這是一個切面
public class ServiceMethodAspect {

    /**
     * 方法鏈接點(處理被 @ServiceMethodAspectAnno 註解的方法)
     */
    @Pointcut("@annotation(org.mizhou.aop.aspect.anno.ServiceMethodAspectAnno)")
    public void methodPointcut() { }

    /**
     * 切入被 @ServiceMethodAspectAnno 註解的方法
     *
     * @param point 鏈接點
     *
     * @return 方法返回值
     * @throws Throwable 可能拋出的異常
     */
    @Around("methodPointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 方法不匹配,即不是要處理的業務方法
        if (!isMatched(point)) {
            // 方法不匹配時的執行動做
            onMismatch(point);
            // 直接執行該方法並返回結果
            return point.proceed();
        }
        
        // 方法返回值
        Object result;
        // 是否拋出異常
        boolean thrown = false;
        // 記下開始執行的時間
        long startTime = System.currentTimeMillis();
        try {
            // 執行目標方法
            result = point.proceed();
        } catch (Throwable e) {
            // 記錄拋出了異常
            thrown = true;
            // 處理異常
            onThrow(point, e);
            // 拋出異常的狀況下,則構造一個返回值的實例,用於業務服務方法的返回
            result = getOnThrown(point, e);
        }

        // 切面結束
        onComplete(point, startTime, thrown, result);

        return result;
    }

    /**
     * 是不是匹配的方法<br/>
     * 限定方法類型入參匹配 BaseRequest,返回值匹配 BaseResponse
     * 
     * @param point 方法的鏈接點
     * @return 是能夠處理的方法返回 true,不然返回 false
     */
    private boolean isMatched(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class returnType = signature.getReturnType();

        // returnType 是 BaseResponse 或其子類型
        if (BaseResponse.class.isAssignableFrom(returnType)) {
            Class[] parameterTypes = signature.getParameterTypes();

            // 參數必須是 BaseRequest 或其子類型
            return parameterTypes.length == 1
                    && BaseRequest.class.isAssignableFrom(parameterTypes[0]);
        }

        return false;
    }

      /**
     * 若是是不要處理的方法,執行的動做
     *
     * @param point 方法的鏈接點
     */
    private void onMismatch(ProceedingJoinPoint point) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        logger.warn("{} 不是 @{} 能夠處理的方法", logTag, ServiceMethodAspectAnno.class.getSimpleName());
    }
    
    /**
     * 拋出異常時,執行的動做
     *
     * @param point 方法的鏈接點
     * @param e 拋出的異常
     */
    private void onThrow(ProceedingJoinPoint point, Throwable e) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        logger.error("{} 調用出錯", logTag, e);
    }

    /**
     * 構建拋出異常時的返回值
     *
     * @param point 方法的鏈接點
     * @param e 拋出的異常
     * @return 拋出異常時的返回值
     */
    @SuppressWarnings("unchecked")
    private BaseResponse getOnThrown(ProceedingJoinPoint point, Throwable e) throws Exception {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<? extends BaseResponse> returnType = signature.getReturnType();

        BaseResponse response = returnType.newInstance();
        response.setPrompt(e.getMessage());
        response.setSuccess(false);

        return response;
    }

    /**
     * 切面完成時,執行的動做
     *
     * @param point 方法的鏈接點
     * @param startTime 執行的開始時間
     * @param thrown 是否拋出異常
     * @param result 執行得到的結果
     */
    private void onComplete(ProceedingJoinPoint point, long startTime, boolean thrown, Object result) {
        BaseResponse response = (BaseResponse) result;

        // 設置方法調用的時間
        response.setSysTime(startTime);
        // 設置方法調用的機器
        response.setHost(getHost());
        // 設置方法調用耗時
        response.setCostTime(System.currentTimeMillis() - startTime);

        Logger logger = getLogger(point);
        // point.getArgs() 得到方法調用入參
        Object request = point.getArgs()[0];
        // 記錄方法調用信息
        logger.info("{}, request={}, response={}", getLogTag(point), request, response);
    }

    /**
     * 模擬得到服務器名稱
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }
    
    /**
     * 得到被代理對象的 Logger
     * 
     * @param point 鏈接點
     * @return 被代理對象的 Logger
     */
    private Logger getLogger(ProceedingJoinPoint point) {
        // 得到被代理對象
        Object target = point.getTarget();
        return LoggerFactory.getLogger(target.getClass());
    }

    /**
     * LogTag = 類名.方法名
     *
     * @param point 鏈接點
     * @return 目標類名.執行方法名
     */
    private String getLogTag(ProceedingJoinPoint point) {
        Object target = point.getTarget();
        String className = target.getClass().getSimpleName();

        MethodSignature signature = (MethodSignature) point.getSignature();
        String methodName = signature.getName();

        return className + "." + methodName;
    }
}

最後咱們就能夠簡化咱們的業務方法了:

@ServiceMethodAspectAnno
public DivisionResponse divide(DivisionRequest request) throws Exception {
    DivisionResponse response = new DivisionResponse();

    // 請求參數
    int dividend = request.getDividend();
    int divisor = request.getDivisor();

    // 模擬檢查業務參數
    // ...檢查業務參數...
    TimeUnit.MILLISECONDS.sleep(300);

    // 模擬執行業務
    int result = dividend / divisor;

    // 設置業務執行結果
    response.setData(result);

    return response;
}

能夠看到,目前業務方法只保留了業務相關的邏輯,而且方法上使用了 @ServiceMethodAspectAnno 進行註解。原來的 記錄方法開始時間捕獲異常並處理打印錯誤日誌記錄方法耗時 等功能,都被放到了切面當中。

驗證切面

如今來驗證下此時切面是否能夠按預期工做。先加入一個新的 Service 以及其實現,用於驗證切面ServiceMethodAspect 是否可以正確篩選出要處理的方法。

NumberService.java

public interface NumberService {

    /**
     * 除法運算
     *
     * @param dividend 被除數
     * @param divisor 除數
     * @return 商
     * @throws Exception 可能產生的異常(切面會捕獲)
     */
    int divide(int dividend, int divisor) throws Exception;

}

NumberServiceImpl.java

@Service
public class NumberServiceImpl implements NumberService {

    @Override
      @ServiceMethodAspectAnno // 測試切面可以篩選方法
    public int divide(int dividend, int divisor) throws Exception {
        // 模擬檢查業務參數
        // ...檢查業務參數...
        TimeUnit.MILLISECONDS.sleep(300);

        // 模擬執行業務
        int result = dividend / divisor;

        return result;
    }

}

由於咱們限定了能夠被織入的方法必須參數爲 BaseRequest,且返回值爲 BaseResponse —— 顯然 NumberService.divide 由於返回的是 int 不知足這一點。

DemoController 中再增長一個處理請求的方法:

@RestController
public class DemoController {
    
      ......
      
    @Resource
    private NumberService numberService;

    @GetMapping("another.do")
    public Integer doAnotherDivision(@RequestParam int a,
                                     @RequestParam int b) throws Exception {
        return numberService.divide(a, b);
    }

}

重啓 SpringBoot 應用:

調用正常時(http://localhost:8080/division.do?a=2&b=1):

正常調用

調用出錯時(http://localhost:8080/division.do?a=2&b=0):

調用出錯

測試與註解不匹配的方法(http://localhost:8080/another.do?a=2&b=1):

錯誤匹配

滿意~ 這下再加入新的業務方法,就不用再在每一個方法中寫那些與業務無關的功能代碼了,直接一個註解搞定~

滿意

擴展方案

問題

原本開開心心能夠收工了,也不知道是誰忽然在我腦子裏發出了一個聲音:若是下次其餘方面的業務,入參不是 BaseRequest,返回值不是 BaseResponse,或者要在 onThrow 時記錄不一樣的日誌 —— 那麼使用上面的方案,是否是要編寫一個新的切面?

大腦忽然一片空白

也是, isMatchedonMismatchonThrowonComplete 這些方法,是每一個切面都會有的。而且對於不一樣的業務,可能會有不一樣的實現,因此應該由一個更加通用的方案,方便未來進行擴展。

思考

咱們通常用的註解,像下面這樣子的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

都是能夠指定參數的。那麼咱們不也能夠在 @ServiceMethodAspectAnno 中,指定一個 處理類,專門用來處理一種類型的業務方法嗎?靈感突現:

  1. 能夠將 isMatchedonMismatchonThrowgetOnThrowonComplete 這些方法,放到一個方法切面處理器接口中
  2. 而後不一樣業務方法的切面處理器,都去實現這個接口,針對本身的業務場景實現處理器的每一個方法
  3. 提供一些方法的默認實現,例如 onMismatchonThrow,這兩個方法通常都是記錄下相應的日誌

實現

首先咱們定義方法切面處理器的接口 MethodAspectProcessor<R>

/**
 * 方法切面處理器
 */
public interface MethodAspectProcessor<R> {

    /**
     * 是不是要處理的方法
     *
     * @param point 方法的鏈接點
     * @return 是要處理的方法返回 true,不然返回 false
     */
    boolean isMatched(ProceedingJoinPoint point);

    /**
     * 若是是不要處理的方法,執行的動做
     *
     * @param point 方法的鏈接點
     */
    default void onMismatch(ProceedingJoinPoint point) {

    }

    // 下面的方法,只在 isMatched 返回 true 時有效

    /**
     * 執行以前的動做<br>
     *
     * @param point 方法的鏈接點
     * @return 返回 true 則表示繼續向下執行;返回 false 則表示禁止調用目標方法,
     * 方法切面處理會此時會先調用 getOnForbid 方法得到被禁止執行時的返回值,而後調用 onComplete 方法結束切面
     */
    default boolean onBefore(ProceedingJoinPoint point) {
        return true;
    }

    /**
     * 禁止調用目標方法時(onBefore 返回 false 時),執行該方法構建返回值
     *
     * @param point 方法的鏈接點
     * @return 禁止調用目標方法時的返回值
     */
    default R getOnForbid(ProceedingJoinPoint point) {
        return null;
    }

    /**
     * 拋出異常時,執行的動做
     *
     * @param point 方法的鏈接點
     * @param e     拋出的異常
     */
    void onThrow(ProceedingJoinPoint point, Throwable e);

    /**
     * 構建拋出異常時的返回值
     *
     * @param point 方法的鏈接點
     * @param e     拋出的異常
     * @return 拋出異常時的返回值
     */
    R getOnThrow(ProceedingJoinPoint point, Throwable e);

    /**
     * 切面完成時,執行的動做
     *
     * @param point     方法的鏈接點
     * @param startTime 執行的開始時間
     * @param forbidden 目標方法是否被禁止執行
     * @param thrown    目標方法執行時是否拋出異常
     * @param result    執行得到的結果
     */
    default void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, R result) {

    }

}

接着咱們改造下 @ServiceMethodAspectAnno,由於咱們如今應該是在作一個通用的方法處理器了,因此先給它更名叫 @MethodAspectAnno,而後加入表示方法切面處理器的字段:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAspectAnno {
    Class<? extends MethodAspectProcessor> value();
}

而後提供一個 MethodAspectProcessor 抽象類 AbstractMethodAspectProcessor<R>,包括了 onMismatchonThrow 的默認實現:

/**
 * 提供默認的兩個功能:<br/>
 * (1)方法不匹配時記錄日誌<br/>
 * (2)目標方法拋出異常時記錄日誌
 */
public abstract class AbstractMethodAspectProcessor<R> implements MethodAspectProcessor<R> {

    @Override
    public void onMismatch(ProceedingJoinPoint point) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        // 得到方法簽名
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 得到方法
        Method method = signature.getMethod();
        // 得到方法的 @MethodAspectAnno 註解
        MethodAspectAnno anno = method.getAnnotation(MethodAspectAnno.class);
        // 得到方法切面處理器的 Class
        Class<? extends MethodAspectProcessor> processorType = anno.value();

        String processorName = processorType.getSimpleName();

        // 若是是接口或者抽象類
        if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
            logger.warn("{} 須要指定具體的切面處理器,由於 {} 是接口或者抽象類", logTag, processorName);
            return;
        }

        logger.warn("{} 不是 {} 能夠處理的方法,或者 {} 在 Spring 容器中不存在", logTag, processorName, processorName);
    }

    @Override
    public void onThrow(ProceedingJoinPoint point, Throwable e) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        logger.error("{} 執行時出錯", logTag, e);
    }

    /**
     * 得到被代理類的 Logger
     *
     * @param point 鏈接點
     * @return 被代理類的 Logger
     */
    protected Logger getLogger(ProceedingJoinPoint point) {
        Object target = point.getTarget();

        return LoggerFactory.getLogger(target.getClass());
    }

    /**
     * LogTag = 類名.方法名
     *
     * @param point 鏈接點
     * @return 目標類名.執行方法名
     */
    protected String getLogTag(ProceedingJoinPoint point) {
        Object target = point.getTarget();
        String className = target.getClass().getSimpleName();

        MethodSignature signature = (MethodSignature) point.getSignature();
        String methodName = signature.getName();

        return className + "." + methodName;
    }
}

再提供一個方法不匹配時的實現 MismatchMethodAspectProcessor<R>,做爲接口的默認實現:

/**
 * 方法不匹配時的方法切面處理器<br/>
 * isMatched 方法返回 false,即不會對任何方法作處理<br/>
 * 方法執行以前,會調用 onMismatch 方法,該方法在 AbstractMethodAspectProcessor 提供默認實現
 */
@Component
public class MismatchMethodAspectProcessor<R> extends AbstractMethodAspectProcessor<R> {

    @Override
    public boolean isMatched(ProceedingJoinPoint point) {
        return false;
    }

    @Override
    public R getOnThrow(ProceedingJoinPoint point, Throwable e) {
        // 不會被調用
        return null;
    }
}

此時咱們再定義 DemoService 中方法的專用方法切面處理器 ServiceMethodProcessor,把以前方案中的代碼拿過來就行:

/**
 * 業務方法切面處理器
 */
@Component
public class ServiceMethodProcessor extends AbstractMethodAspectProcessor<BaseResponse> {

    /**
     * 是不是要處理的方法<br/>
     * 限定方法類型入參匹配 BaseRequest,返回值匹配 BaseResponse
     *
     * @param point 方法的鏈接點
     * @return 是要處理的方法返回 true,不然返回 false
     */
    @Override
    public boolean isMatched(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class returnType = signature.getReturnType();

        // returnType 是 BaseResponse 或其子類型
        if (BaseResponse.class.isAssignableFrom(returnType)) {
            Class[] parameterTypes = signature.getParameterTypes();

            // 參數必須是 BaseRequest 或其子類型
            return parameterTypes.length == 1
                    && BaseRequest.class.isAssignableFrom(parameterTypes[0]);
        }

        return false;
    }

    /**
     * 構建拋出異常時的返回值<br/>
     *
     * @param point 方法的鏈接點
     * @param e 拋出的異常
     * @return 拋出異常時的返回值
     */
    @Override
    @SuppressWarnings("unchecked")
    public BaseResponse getOnThrow(ProceedingJoinPoint point, Throwable e) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<? extends BaseResponse> returnType = signature.getReturnType();

        // 構造拋出異常時的返回值
        BaseResponse response = newInstance(returnType);

        response.setPrompt(e.getMessage());
        response.setSuccess(false);

        return response;
    }

    /**
     * 切面完成時,執行的動做
     *
     * @param point 方法的鏈接點
     * @param startTime 執行的開始時間
     * @param result 執行得到的結果
     */
    @Override
    public void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, BaseResponse result) {
        // 設置方法調用的時間
        result.setSysTime(startTime);
        // 設置方法調用的機器
        result.setHost(getHost());
        // 設置方法調用耗時
        result.setCostTime(System.currentTimeMillis() - startTime);

        Logger logger = getLogger(point);
        // point.getArgs() 得到方法調用入參
        Object request = point.getArgs()[0];
        // 記錄方法調用信息
        logger.info("{}, request={}, response={}", getLogTag(point), request, result);
    }

    private BaseResponse newInstance(Class<? extends BaseResponse> type) {
        try {
            return type.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            return new CommonResponse();
        }
    }

    /**
     * 模擬得到服務器名稱
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

}

咱們還須要一個方法,來經過註解獲取 和被註解方法匹配的 方法切面處理器,在 MethodAspectProcessor 加入一個靜態方法:

/**
 * 經過註解獲取 和被註解方法匹配的 切面處理器
 *
 * @param anno 註解
 * @return 匹配的切面處理器
 * @throws Exception 反射建立切面處理器時的異常
 */
static MethodAspectProcessor from(MethodAspectAnno anno) throws Exception {
    Class<? extends MethodAspectProcessor> processorType = anno.value();

    // 若是指定的是接口或者抽象類(即便用方非要搞事情)
    if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
        processorType = MismatchMethodAspectProcessor.class;
    }

    return processorType.newInstance();
}

修改下以前的方法切面,一樣的,由於該方法切面不只僅是能夠處理 Service 方法了,因而更名叫 MethodAspect。經過在 @Around 中加入 @annotation(anno),能夠將註解實例注入到參數中:

@Aspect
@Component
public class MethodAspect {

    /**
     * 方法鏈接點(處理被 @MethodAspectAnno 註解的方法)
     */
    @Pointcut("@annotation(org.mizhou.aop.aspect.anno.MethodAspectAnno)")
    public void methodPointcut() { }

    /**
     * 切入被 @MethodAspectAnno 註解的方法
     *
     * @param point 鏈接點
     * @param anno 註解
     * 
     * @return 方法返回值
     * @throws Throwable 可能拋出的異常
     */
    @Around("methodPointcut() && @annotation(anno)")
    public Object doAround(ProceedingJoinPoint point, MethodAspectAnno anno) throws Throwable {
        // 經過註解獲取處理器
        MethodAspectProcessor processor = MethodAspectProcessor.from(anno);

        // 方法不匹配,即不是要處理的業務方法
        if (!processor.isMatched(point)) {
            // 方法不匹配時的執行動做
            processor.onMismatch(point);
            // 直接執行該方法並返回結果
            return point.proceed();
        }

        // 執行以前
        boolean permitted = processor.onBefore(point);
        // 開始執行的時間
        long startTime = System.currentTimeMillis();

        // 方法返回值
        Object result;
        // 是否拋出了異常
        boolean thrown = false;

        // 目標方法被容許執行
        if (permitted) {
            try {
                // 執行目標方法
                result = point.proceed();
            } catch (Throwable e) {
                // 拋出異常
                thrown = true;
                // 處理異常
                processor.onThrow(point, e);
                // 拋出異常的狀況下,則構造一個返回值的實例,用於業務服務方法的返回
                result = processor.getOnThrow(point, e);
            }
        }
        // 目標方法被禁止執行
        else {
            // 禁止執行時的返回值
            result = processor.getOnForbid(point);
        }

        // 切面結束
        processor.onComplete(point, startTime, !permitted, thrown, result);

        return result;
    }
}

最後在 DemoServiceImpl 的業務方法上,應用 @MethodAspectAnno,並指定處理方法的方法切面處理器:

@MethodAspectAnno(ServiceMethodProcessor.class)
public DivisionResponse divide(DivisionRequest request) throws Exception {
    DivisionResponse response = new DivisionResponse();

    // 請求參數
    int dividend = request.getDividend();
    int divisor = request.getDivisor();

    // 模擬檢查業務參數
    // ...檢查業務參數...
    TimeUnit.MILLISECONDS.sleep(300);

    // 模擬執行業務
    int result = dividend / divisor;

    // 設置業務執行結果
    response.setData(result);

    return response;
}

以及在不匹配的方法上,應用 @MethodAspectAnno(ServiceMethodProcessor.class)

@Service
public class NumberServiceImpl implements NumberService {

    @Override
      // 不匹配的方法處理器
    @MethodAspectAnno(ServiceMethodProcessor.class)
    public int divide(int dividend, int divisor) throws Exception {
        // 模擬檢查業務參數
        // ...檢查業務參數...
        TimeUnit.MILLISECONDS.sleep(300);

        // 模擬執行業務
        int result = dividend / divisor;

        return result;
    }

}

大功告成,來測試一下:

正常調用(http://localhost:8080/division.do?a=2&b=1):

正常調用的狀況

調用出錯(http://localhost:8080/division.do?a=2&b=0):

調用出錯的狀況

測試與切面處理器不匹配的方法(http://localhost:8080/another.do?a=2&b=1):

方法不匹配

優化

此時個人耳邊又響起了一個聲音(爲何我想的老是這麼多...):

心情複雜

不論是 MismatchMethodAspectProcessor 仍是用於業務方法的 ServiceMethodProcessor,或者未來定義的一些其餘的 MethodAspectProcessor,它們由於沒有定義變量或者沒有與其餘類分享變量,因此它們是線程安全的,不必每次在執行切面調用時,都去新建一個對應的方法切面處理器。

緩存

因而想到了 Netty 裏面的 @Sharable,用來標記一個 ChannelHandler 是可共享的。因此咱們也能夠先定義一個 @Sharble 註解,用來標記一個 MethodAspectProcessor 是可共享的,即線程安全的。而後對被 @Sharable 註解的方法處理器,進行緩存 —— 緩存的鍵就是方法切面處理器的 Class,值就是方法處理器的實例。定義 @Sharable 註解:

/**
 * 標記一個類可共享
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sharable {
    
}

而後修改 MethodAspectProcessor 中從註解獲取方法切面處理器的 from 方法:

public interface MethodAspectProcessor<R> {

    /**
     * 用於緩存被 @Sharable 註解的 MethodAspectProcessor(即線程安全可共享的)
     */
    Map<Class, MethodAspectProcessor> PROCESSOR_CACHE = new ConcurrentHashMap<>();
  
      ......

    /**
     * 獲取 和被註解方法匹配的 切面處理器
     *
     * @param anno 註解
     * @return 匹配的切面處理器
     * @throws Exception 反射建立切面處理器時的異常
     */
    static MethodAspectProcessor from(MethodAspectAnno anno) throws Exception {
        // 獲取方法切面處理器的類型
        Class<? extends MethodAspectProcessor> processorType = anno.value();
        Sharable sharableAnno = processorType.getAnnotation(Sharable.class);

        // processorType 上存在 @Sharable 註解,方法處理器可共享
        if (sharableAnno != null) {
            // 嘗試先從緩存中獲取
            MethodAspectProcessor processor = PROCESSOR_CACHE.get(processorType);
            // 緩存中存在對應的方法處理器
            if (processor != null) {
                return processor;
            }
        }

        // 若是指定的處理器類是接口或者抽象類
        if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
            processorType = MismatchMethodAspectProcessor.class;
        }

        // 建立切面處理器
        MethodAspectProcessor processor = processorType.newInstance();

        // 處理器可共享
        if (sharableAnno != null) {
            // 對 方法處理器 進行緩存
            PROCESSOR_CACHE.put(processorType, processor);
        }

        return processor;
    }

}

OK,完美,很是滿意~

甜

後記

在最近的實踐中,發現咱們的 MethodAspectProcessor 許多時候都不能脫離 Spring 容器,即須要讓 MethodAspectProcessor 成爲 Spring 容器中的 Bean,從而結合 Spring 容器中的其餘 Bean,完成更加複雜的功能。例如某個方法須要實現 3 秒內防重複調用,咱們便須要使用到緩存,而緩存相關的 Bean 是由 Spring 來管理的。因此咱們如今改造咱們的 AOP 方法,讓全部的 MethodAspectProcessor 都交給 Spring 管理。首先咱們修改各個 MethodAspectProcessor,使用 @Component 註解讓其成爲 Spring 容器中的 Bean:

@Component
public class MismatchMethodAspectProcessor<R> extends AbstractMethodAspectProcessor<R>
@Component
public class ServiceMethodProcessor extends AbstractMethodAspectProcessor<BaseResponse>

修改 MethodAspect,讓其從 Spring 容器中獲取方法切面處理器:

@Aspect
@Component
public class MethodAspect implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private ApplicationContext appContext;

    /**
     * 方法鏈接點(處理被 @MethodAspectAnno 註解的方法)
     */
    @Pointcut("@annotation(xyz.mizhoux.aop.aspect.anno.MethodAspectAnno)")
    public void methodPointcut() { }

    /**
     * 切入被 @MethodAspectAnno 註解的方法
     *
     * @param point 鏈接點
     * @param anno  註解
     * @return 方法返回值
     * @throws Throwable 可能拋出的異常
     */
    @Around("methodPointcut() && @annotation(anno)")
    public Object doAround(ProceedingJoinPoint point, MethodAspectAnno anno) throws Throwable {
        // 經過註解獲取處理器
        MethodAspectProcessor processor = getProcessor(anno);

        .......
    }

    private MethodAspectProcessor getProcessor(MethodAspectAnno anno) {
        Class<? extends MethodAspectProcessor> processorType = anno.value();

        try {
            return appContext.getBean(processorType);
        } catch (BeansException ex) {
            logger.error("{} 在 Spring 容器中不存在", processorType.getName());
        }

        return appContext.getBean(MismatchMethodAspectProcessor.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appContext = applicationContext;
    }
}

本文最終方案的代碼可見:aop-method

相關文章
相關標籤/搜索