基于 AOP 抽離方法的重復(fù)代碼

背景

今天師兄和我說(shuō)摹察,“之葉恩掷,你設(shè)計(jì)一個(gè)方案,把目前業(yè)務(wù)方法中和業(yè)務(wù)無(wú)關(guān)的邏輯都抽離出來(lái)供嚎,讓每個(gè)方法只關(guān)心自己的業(yè)務(wù)邏輯”黄娘。我會(huì)心一笑 ??(因?yàn)槲覀冊(cè)鐟?yīng)該做這件事情了)

邪魅一笑

現(xiàn)有的業(yè)務(wù)方法

之前代碼里每個(gè)業(yè)務(wù)方法幾乎都是長(zhǎng)這樣:

public class XxxServiceImpl implements XxxService {

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

    @Override
    public XxxResponse<...> queryXxx(XxxRequest request) {
        // 記錄方法開(kāi)始時(shí)間
        long startTime = System.currentTimeMillis();
        // 構(gòu)造響應(yīng)
        XxxResponse<PagedData> response = new XxxResponse();
        // 設(shè)置調(diào)用機(jī)器
        response.setHost(ServiceUtils.getHost());
        // 設(shè)置方法開(kāi)始執(zhí)行時(shí)間
        response.setSysTime(startTime);

        try {
            // 業(yè)務(wù)邏輯代碼
            ......

            response.setData(pagedData);
        } catch(Throwable e) {
            // 拋出異常時(shí)候執(zhí)行
            logger.error(...);
            response.failBizInfo(ServiceBizError.UNKNOWN_ERROR);
        } finally {
            // 設(shè)置方法耗時(shí)
            long costTime = System.currentTimeMillis() - startTime;
            response.setCostTime(costTime);
            // 記錄調(diào)用信息
            logger.info(...);
        }
        // 返回響應(yīng)
        return response;
    }
  
    // 后面還有若干個(gè)類(lèi)似的業(yè)務(wù)方法
    ......
}

很容易可以看出,記錄方法開(kāi)始時(shí)間克滴、捕獲異常并處理逼争、打印錯(cuò)誤日志記錄方法耗時(shí) 這些都是和業(yè)務(wù)沒(méi)有關(guān)系的劝赔,業(yè)務(wù)方法關(guān)心的氮凝,只應(yīng)該是 業(yè)務(wù)邏輯代碼 才對(duì)。一兩個(gè)方法這個(gè)樣子看起來(lái)也還好望忆,但是目前項(xiàng)目里面已經(jīng)有十幾個(gè)這種樣子的方法了罩阵,而且以后還會(huì)更多,重復(fù)代碼對(duì)我們簡(jiǎn)直不能忍 —— 是的启摄,我也早就看這些業(yè)務(wù)方法不順眼了稿壁,安排!

必須安排

設(shè)計(jì)方案

AOP 登場(chǎng)

大家都聽(tīng)過(guò) Spring 有兩大神器 —— IoC 和 AOP —— 了解 AOP 的人歉备,都知道 AOP 是 Aspect Oriented Programming傅是,即面向切面編程:通過(guò)預(yù)編譯方式(CGLib)或者運(yùn)行期動(dòng)態(tài)代理(JDK Proxy)來(lái)實(shí)現(xiàn)程序功能代理的技術(shù)。此時(shí)的情況蕾羊,就完美匹配 AOP 的應(yīng)用場(chǎng)景喧笔。我們可以定義一個(gè)切點(diǎn)(PointCut,也叫連接點(diǎn))龟再,然后對(duì)和 切點(diǎn)匹配的方法书闸,織入(Weaving)切面(Aspect),進(jìn)行增強(qiáng)(Advice)處理:即在方法 調(diào)用前利凑、調(diào)用后 或者 拋出異常時(shí)浆劲,進(jìn)行額外的處理。

實(shí)現(xiàn)方案

搭建示例項(xiàng)目

為了方便示例哀澈,首先我們建立一個(gè)簡(jiǎn)單的 SpringBoot 項(xiàng)目牌借,并添加示例的 Service 和 Controller:

創(chuàng)建 SpringBoot 項(xiàng)目

加入一個(gè) DemoService:

public interface DemoService {

    /**
     * 除法運(yùn)算
     *
     * @param request 除法運(yùn)算請(qǐng)求
     * @return 除法運(yùn)算結(jié)果
     */
    DivisionResponse divide(DivisionRequest request);

}

DemoService 的實(shí)現(xiàn):

@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();
        // 設(shè)置方法調(diào)用的時(shí)間
        response.setSysTime(startTime);
        // 設(shè)置方法調(diào)用的機(jī)器
        response.setHost(getHost());

        // 請(qǐng)求參數(shù)
        int dividend = request.getDividend();
        int divisor = request.getDivisor();

        try {
            // 模擬檢查業(yè)務(wù)參數(shù)
            // ...檢查業(yè)務(wù)參數(shù)...
            TimeUnit.MILLISECONDS.sleep(300);

            // 模擬執(zhí)行業(yè)務(wù)
            int result = dividend / divisor;

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

        return response;
    }

    /**
     * 模擬獲得服務(wù)器名稱(chēng)
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }
}

再加入一個(gè) DemoController:

@RestController
public class DemoController {

    @Resource
    private DemoService demoService;

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

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

啟動(dòng)應(yīng)用,看一下目前調(diào)用業(yè)務(wù)方法時(shí)的情況:

  1. 調(diào)用正常情況(a=2割按,b=1)

    正常情況
  2. 調(diào)用出錯(cuò)情況(a=2膨报,b=0)

    錯(cuò)誤情況

編寫(xiě)切面

現(xiàn)在的 Java Web 應(yīng)用,使用注解來(lái)進(jìn)行配置和做 AOP 已經(jīng)是主流 —— 因?yàn)橄啾?XML,注解更簡(jiǎn)單而且更好用现柠。所以我們先定義一個(gè) @ServiceMethodAspectAnno

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

這個(gè)注解的目標(biāo)類(lèi)型是 方法院领,并且在 運(yùn)行期 保留。然后我們就可以來(lái)定義切面了晒旅,這個(gè)切面會(huì)攔截所有被 @ServiceMethodAspectAnno 注解的方法,并做織入處理:

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

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

    /**
     * 切入被 @ServiceMethodAspectAnno 注解的方法
     *
     * @param point 連接點(diǎn)
     *
     * @return 方法返回值
     * @throws Throwable 可能拋出的異常
     */
    @Around("methodPointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 方法不匹配汪诉,即不是要處理的業(yè)務(wù)方法
        if (!isMatched(point)) {
            // 方法不匹配時(shí)的執(zhí)行動(dòng)作
            onMismatch(point);
            // 直接執(zhí)行該方法并返回結(jié)果
            return point.proceed();
        }
        
        // 方法返回值
        Object result;
        // 是否拋出異常
        boolean thrown = false;
        // 記下開(kāi)始執(zhí)行的時(shí)間
        long startTime = System.currentTimeMillis();
        try {
            // 執(zhí)行目標(biāo)方法
            result = point.proceed();
        } catch (Throwable e) {
            // 記錄拋出了異常
            thrown = true;
            // 處理異常
            onThrow(point, e);
            // 拋出異常的情況下废恋,則構(gòu)造一個(gè)返回值的實(shí)例,用于業(yè)務(wù)服務(wù)方法的返回
            result = getOnThrown(point, e);
        }

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

        return result;
    }

    /**
     * 是否是匹配的方法<br/>
     * 限定方法類(lèi)型入?yún)⑵ヅ?BaseRequest扒寄,返回值匹配 BaseResponse
     * 
     * @param point 方法的連接點(diǎn)
     * @return 是可以處理的方法返回 true鱼鼓,否則返回 false
     */
    private boolean isMatched(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class returnType = signature.getReturnType();

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

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

        return false;
    }

    /**
     * 如果是不要處理的方法,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     */
    private void onMismatch(ProceedingJoinPoint point) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        logger.warn("{} 不是 @{} 可以處理的方法", logTag, ServiceMethodAspectAnno.class.getSimpleName());
    }
    
    /**
     * 拋出異常時(shí)该编,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     * @param e 拋出的異常
     */
    private void onThrow(ProceedingJoinPoint point, Throwable e) {
        Logger logger = getLogger(point);
        String logTag = getLogTag(point);

        logger.error("{} 調(diào)用出錯(cuò)", logTag, e);
    }

    /**
     * 構(gòu)建拋出異常時(shí)的返回值
     *
     * @param point 方法的連接點(diǎn)
     * @param e 拋出的異常
     * @return 拋出異常時(shí)的返回值
     */
    @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;
    }

    /**
     * 切面完成時(shí)迄本,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     * @param startTime 執(zhí)行的開(kāi)始時(shí)間
     * @param thrown 是否拋出異常
     * @param result 執(zhí)行獲得的結(jié)果
     */
    private void onComplete(ProceedingJoinPoint point, long startTime, boolean thrown, Object result) {
        BaseResponse response = (BaseResponse) result;

        // 設(shè)置方法調(diào)用的時(shí)間
        response.setSysTime(startTime);
        // 設(shè)置方法調(diào)用的機(jī)器
        response.setHost(getHost());
        // 設(shè)置方法調(diào)用耗時(shí)
        response.setCostTime(System.currentTimeMillis() - startTime);

        Logger logger = getLogger(point);
        // point.getArgs() 獲得方法調(diào)用入?yún)?        Object request = point.getArgs()[0];
        // 記錄方法調(diào)用信息
        logger.info("{}, request={}, response={}", getLogTag(point), request, response);
    }

    /**
     * 模擬獲得服務(wù)器名稱(chēng)
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }
    
    /**
     * 獲得被代理對(duì)象的 Logger
     * 
     * @param point 連接點(diǎn)
     * @return 被代理對(duì)象的 Logger
     */
    private Logger getLogger(ProceedingJoinPoint point) {
        // 獲得被代理對(duì)象
        Object target = point.getTarget();
        return LoggerFactory.getLogger(target.getClass());
    }

    /**
     * LogTag = 類(lèi)名.方法名
     *
     * @param point 連接點(diǎn)
     * @return 目標(biāo)類(lèi)名.執(zhí)行方法名
     */
    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;
    }
}

最后我們就可以簡(jiǎn)化我們的業(yè)務(wù)方法了:

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

    // 請(qǐng)求參數(shù)
    int dividend = request.getDividend();
    int divisor = request.getDivisor();

    // 模擬檢查業(yè)務(wù)參數(shù)
    // ...檢查業(yè)務(wù)參數(shù)...
    TimeUnit.MILLISECONDS.sleep(300);

    // 模擬執(zhí)行業(yè)務(wù)
    int result = dividend / divisor;

    // 設(shè)置業(yè)務(wù)執(zhí)行結(jié)果
    response.setData(result);

    return response;
}

可以看到,目前業(yè)務(wù)方法只保留了業(yè)務(wù)相關(guān)的邏輯课竣,并且方法上使用了 @ServiceMethodAspectAnno 進(jìn)行注解嘉赎。原來(lái)的 記錄方法開(kāi)始時(shí)間捕獲異常并處理于樟、打印錯(cuò)誤日志公条、記錄方法耗時(shí) 等功能,都被放到了切面當(dāng)中迂曲。

驗(yàn)證切面

現(xiàn)在來(lái)驗(yàn)證下此時(shí)切面是否可以按預(yù)期工作靶橱。先加入一個(gè)新的 Service 以及其實(shí)現(xiàn),用于驗(yàn)證切面ServiceMethodAspect 是否能夠正確篩選出要處理的方法路捧。

NumberService.java

public interface NumberService {

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

}

NumberServiceImpl.java

@Service
public class NumberServiceImpl implements NumberService {

    @Override
      @ServiceMethodAspectAnno // 測(cè)試切面能夠篩選方法
    public int divide(int dividend, int divisor) throws Exception {
        // 模擬檢查業(yè)務(wù)參數(shù)
        // ...檢查業(yè)務(wù)參數(shù)...
        TimeUnit.MILLISECONDS.sleep(300);

        // 模擬執(zhí)行業(yè)務(wù)
        int result = dividend / divisor;

        return result;
    }

}

因?yàn)槲覀兿薅丝梢员豢椚氲姆椒ū仨殔?shù)為 BaseRequest关霸,且返回值為 BaseResponse —— 顯然 NumberService.divide 因?yàn)榉祷氐氖?int 不滿(mǎn)足這一點(diǎn)。

DemoController 中再增加一個(gè)處理請(qǐng)求的方法:

@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 應(yīng)用:

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

正常調(diào)用

調(diào)用出錯(cuò)時(shí)(http://localhost:8080/division.do?a=2&b=0):

調(diào)用出錯(cuò)

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

錯(cuò)誤匹配

滿(mǎn)意~ 這下再加入新的業(yè)務(wù)方法杰扫,就不用再在每個(gè)方法中寫(xiě)那些與業(yè)務(wù)無(wú)關(guān)的功能代碼了队寇,直接一個(gè)注解搞定~

滿(mǎn)意

擴(kuò)展方案

問(wèn)題

本來(lái)開(kāi)開(kāi)心心可以收工了,也不知道是誰(shuí)突然在我腦子里發(fā)出了一個(gè)聲音:如果下次其他方面的業(yè)務(wù)章姓,入?yún)⒉皇?BaseRequest命满,返回值不是 BaseResponse削锰,或者要在 onThrow 時(shí)記錄不同的日志 —— 那么使用上面的方案,是不是要編寫(xiě)一個(gè)新的切面?

大腦突然一片空白

也是坛悉, isMatchedonMismatch蘑拯、onThrow厌衙、onComplete 這些方法,是每個(gè)切面都會(huì)有的笨觅。并且對(duì)于不同的業(yè)務(wù)拦耐,可能會(huì)有不同的實(shí)現(xiàn)耕腾,所以應(yīng)該由一個(gè)更加通用的方案,方便將來(lái)進(jìn)行擴(kuò)展杀糯。

思考

我們一般用的注解扫俺,像下面這樣子的:

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

都是可以指定參數(shù)的。那么我們不也可以在 @ServiceMethodAspectAnno 中固翰,指定一個(gè) 處理類(lèi)狼纬,專(zhuān)門(mén)用來(lái)處理一種類(lèi)型的業(yè)務(wù)方法嗎?靈感突現(xiàn):

  1. 可以將 isMatched骂际、onMismatch疗琉、onThrowgetOnThrow歉铝,onComplete 這些方法盈简,放到一個(gè)方法切面處理器接口中
  2. 然后不同業(yè)務(wù)方法的切面處理器,都去實(shí)現(xiàn)這個(gè)接口太示,針對(duì)自己的業(yè)務(wù)場(chǎng)景實(shí)現(xiàn)處理器的每個(gè)方法
  3. 提供一些方法的默認(rèn)實(shí)現(xiàn)柠贤,例如 onMismatchonThrow,這兩個(gè)方法一般都是記錄下相應(yīng)的日志

實(shí)現(xiàn)

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

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

    /**
     * 是否是要處理的方法
     *
     * @param point 方法的連接點(diǎn)
     * @return 是要處理的方法返回 true类缤,否則返回 false
     */
    boolean isMatched(ProceedingJoinPoint point);

    /**
     * 如果是不要處理的方法种吸,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     */
    default void onMismatch(ProceedingJoinPoint point) {

    }

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

    /**
     * 執(zhí)行之前的動(dòng)作<br>
     *
     * @param point 方法的連接點(diǎn)
     * @return 返回 true 則表示繼續(xù)向下執(zhí)行呀非;返回 false 則表示禁止調(diào)用目標(biāo)方法坚俗,
     * 方法切面處理會(huì)此時(shí)會(huì)先調(diào)用 getOnForbid 方法獲得被禁止執(zhí)行時(shí)的返回值,然后調(diào)用 onComplete 方法結(jié)束切面
     */
    default boolean onBefore(ProceedingJoinPoint point) {
        return true;
    }

    /**
     * 禁止調(diào)用目標(biāo)方法時(shí)(onBefore 返回 false 時(shí))岸裙,執(zhí)行該方法構(gòu)建返回值
     *
     * @param point 方法的連接點(diǎn)
     * @return 禁止調(diào)用目標(biāo)方法時(shí)的返回值
     */
    default R getOnForbid(ProceedingJoinPoint point) {
        return null;
    }

    /**
     * 拋出異常時(shí)猖败,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     * @param e     拋出的異常
     */
    void onThrow(ProceedingJoinPoint point, Throwable e);

    /**
     * 構(gòu)建拋出異常時(shí)的返回值
     *
     * @param point 方法的連接點(diǎn)
     * @param e     拋出的異常
     * @return 拋出異常時(shí)的返回值
     */
    R getOnThrow(ProceedingJoinPoint point, Throwable e);

    /**
     * 切面完成時(shí),執(zhí)行的動(dòng)作
     *
     * @param point     方法的連接點(diǎn)
     * @param startTime 執(zhí)行的開(kāi)始時(shí)間
     * @param forbidden 目標(biāo)方法是否被禁止執(zhí)行
     * @param thrown    目標(biāo)方法執(zhí)行時(shí)是否拋出異常
     * @param result    執(zhí)行獲得的結(jié)果
     */
    default void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, R result) {

    }

}

接著我們改造下 @ServiceMethodAspectAnno降允,因?yàn)槲覀儸F(xiàn)在應(yīng)該是在做一個(gè)通用的方法處理器了恩闻,所以先給它改名叫 @MethodAspectAnno,然后加入表示方法切面處理器的字段:

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

然后提供一個(gè) MethodAspectProcessor 抽象類(lèi) AbstractMethodAspectProcessor<R>剧董,包括了 onMismatchonThrow 的默認(rèn)實(shí)現(xiàn):

/**
 * 提供默認(rèn)的兩個(gè)功能:<br/>
 * (1)方法不匹配時(shí)記錄日志<br/>
 * (2)目標(biāo)方法拋出異常時(shí)記錄日志
 */
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();

        // 如果是接口或者抽象類(lèi)
        if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
            logger.warn("{} 需要指定具體的切面處理器幢尚,因?yàn)?{} 是接口或者抽象類(lèi)", 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("{} 執(zhí)行時(shí)出錯(cuò)", logTag, e);
    }

    /**
     * 獲得被代理類(lèi)的 Logger
     *
     * @param point 連接點(diǎn)
     * @return 被代理類(lèi)的 Logger
     */
    protected Logger getLogger(ProceedingJoinPoint point) {
        Object target = point.getTarget();

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

    /**
     * LogTag = 類(lèi)名.方法名
     *
     * @param point 連接點(diǎn)
     * @return 目標(biāo)類(lèi)名.執(zhí)行方法名
     */
    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;
    }
}

再提供一個(gè)方法不匹配時(shí)的實(shí)現(xiàn) MismatchMethodAspectProcessor<R>翅楼,作為接口的默認(rèn)實(shí)現(xiàn):

/**
 * 方法不匹配時(shí)的方法切面處理器<br/>
 * isMatched 方法返回 false尉剩,即不會(huì)對(duì)任何方法做處理<br/>
 * 方法執(zhí)行之前,會(huì)調(diào)用 onMismatch 方法毅臊,該方法在 AbstractMethodAspectProcessor 提供默認(rèn)實(shí)現(xiàn)
 */
@Component
public class MismatchMethodAspectProcessor<R> extends AbstractMethodAspectProcessor<R> {

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

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

此時(shí)我們?cè)俣x DemoService 中方法的專(zhuān)用方法切面處理器 ServiceMethodProcessor理茎,把之前方案中的代碼拿過(guò)來(lái)就行:

/**
 * 業(yè)務(wù)方法切面處理器
 */
@Component
public class ServiceMethodProcessor extends AbstractMethodAspectProcessor<BaseResponse> {

    /**
     * 是否是要處理的方法<br/>
     * 限定方法類(lèi)型入?yún)⑵ヅ?BaseRequest,返回值匹配 BaseResponse
     *
     * @param point 方法的連接點(diǎn)
     * @return 是要處理的方法返回 true,否則返回 false
     */
    @Override
    public boolean isMatched(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class returnType = signature.getReturnType();

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

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

        return false;
    }

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

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

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

        return response;
    }

    /**
     * 切面完成時(shí)皂林,執(zhí)行的動(dòng)作
     *
     * @param point 方法的連接點(diǎn)
     * @param startTime 執(zhí)行的開(kāi)始時(shí)間
     * @param result 執(zhí)行獲得的結(jié)果
     */
    @Override
    public void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, BaseResponse result) {
        // 設(shè)置方法調(diào)用的時(shí)間
        result.setSysTime(startTime);
        // 設(shè)置方法調(diào)用的機(jī)器
        result.setHost(getHost());
        // 設(shè)置方法調(diào)用耗時(shí)
        result.setCostTime(System.currentTimeMillis() - startTime);

        Logger logger = getLogger(point);
        // point.getArgs() 獲得方法調(diào)用入?yún)?        Object request = point.getArgs()[0];
        // 記錄方法調(diào)用信息
        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();
        }
    }

    /**
     * 模擬獲得服務(wù)器名稱(chēng)
     */
    private String getHost() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

}

我們還需要一個(gè)方法朗鸠,來(lái)通過(guò)注解獲取 和被注解方法匹配的 方法切面處理器,在 MethodAspectProcessor 加入一個(gè)靜態(tài)方法:

/**
 * 通過(guò)注解獲取 和被注解方法匹配的 切面處理器
 *
 * @param anno 注解
 * @return 匹配的切面處理器
 * @throws Exception 反射創(chuàng)建切面處理器時(shí)的異常
 */
static MethodAspectProcessor from(MethodAspectAnno anno) throws Exception {
    Class<? extends MethodAspectProcessor> processorType = anno.value();

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

    return processorType.newInstance();
}

修改下之前的方法切面础倍,同樣的烛占,因?yàn)樵摲椒ㄇ忻娌粌H僅是可以處理 Service 方法了,于是改名叫 MethodAspect沟启。通過(guò)在 @Around 中加入 @annotation(anno)忆家,可以將注解實(shí)例注入到參數(shù)中:

@Aspect
@Component
public class MethodAspect {

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

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

        // 方法不匹配,即不是要處理的業(yè)務(wù)方法
        if (!processor.isMatched(point)) {
            // 方法不匹配時(shí)的執(zhí)行動(dòng)作
            processor.onMismatch(point);
            // 直接執(zhí)行該方法并返回結(jié)果
            return point.proceed();
        }

        // 執(zhí)行之前
        boolean permitted = processor.onBefore(point);
        // 開(kāi)始執(zhí)行的時(shí)間
        long startTime = System.currentTimeMillis();

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

        // 目標(biāo)方法被允許執(zhí)行
        if (permitted) {
            try {
                // 執(zhí)行目標(biāo)方法
                result = point.proceed();
            } catch (Throwable e) {
                // 拋出異常
                thrown = true;
                // 處理異常
                processor.onThrow(point, e);
                // 拋出異常的情況下美浦,則構(gòu)造一個(gè)返回值的實(shí)例弦赖,用于業(yè)務(wù)服務(wù)方法的返回
                result = processor.getOnThrow(point, e);
            }
        }
        // 目標(biāo)方法被禁止執(zhí)行
        else {
            // 禁止執(zhí)行時(shí)的返回值
            result = processor.getOnForbid(point);
        }

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

        return result;
    }
}

最后在 DemoServiceImpl 的業(yè)務(wù)方法上项栏,應(yīng)用 @MethodAspectAnno浦辨,并指定處理方法的方法切面處理器:

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

    // 請(qǐng)求參數(shù)
    int dividend = request.getDividend();
    int divisor = request.getDivisor();

    // 模擬檢查業(yè)務(wù)參數(shù)
    // ...檢查業(yè)務(wù)參數(shù)...
    TimeUnit.MILLISECONDS.sleep(300);

    // 模擬執(zhí)行業(yè)務(wù)
    int result = dividend / divisor;

    // 設(shè)置業(yè)務(wù)執(zhí)行結(jié)果
    response.setData(result);

    return response;
}

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

@Service
public class NumberServiceImpl implements NumberService {

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

        // 模擬執(zhí)行業(yè)務(wù)
        int result = dividend / divisor;

        return result;
    }

}

大功告成沼沈,來(lái)測(cè)試一下:

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

正常調(diào)用的情況

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

調(diào)用出錯(cuò)的情況

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

方法不匹配

優(yōu)化

此時(shí)我的耳邊又響起了一個(gè)聲音(為什么我想的總是這么多...):

心情復(fù)雜

不管是 MismatchMethodAspectProcessor 還是用于業(yè)務(wù)方法的 ServiceMethodProcessor流酬,或者將來(lái)定義的一些其他的 MethodAspectProcessor,它們因?yàn)闆](méi)有定義變量或者沒(méi)有與其他類(lèi)分享變量列另,所以它們是線(xiàn)程安全的芽腾,沒(méi)必要每次在執(zhí)行切面調(diào)用時(shí),都去新建一個(gè)對(duì)應(yīng)的方法切面處理器页衙。

緩存

于是想到了 Netty 里面的 @Sharable摊滔,用來(lái)標(biāo)記一個(gè) ChannelHandler 是可共享的。所以我們也可以先定義一個(gè) @Sharble 注解店乐,用來(lái)標(biāo)記一個(gè) MethodAspectProcessor 是可共享的艰躺,即線(xiàn)程安全的。然后對(duì)被 @Sharable 注解的方法處理器眨八,進(jìn)行緩存 —— 緩存的鍵就是方法切面處理器的 Class腺兴,值就是方法處理器的實(shí)例。定義 @Sharable 注解:

/**
 * 標(biāo)記一個(gè)類(lèi)可共享
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sharable {
    
}

然后修改 MethodAspectProcessor 中從注解獲取方法切面處理器的 from 方法:

public interface MethodAspectProcessor<R> {

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

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

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

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

        // 創(chuàng)建切面處理器
        MethodAspectProcessor processor = processorType.newInstance();

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

        return processor;
    }

}

OK页响,完美,非常滿(mǎn)意~

后記

在最近的實(shí)踐中段誊,發(fā)現(xiàn)我們的 MethodAspectProcessor 許多時(shí)候都不能脫離 Spring 容器闰蚕,即需要讓 MethodAspectProcessor 成為 Spring 容器中的 Bean,從而結(jié)合 Spring 容器中的其他 Bean连舍,完成更加復(fù)雜的功能陪腌。例如某個(gè)方法需要實(shí)現(xiàn) 3 秒內(nèi)防重復(fù)調(diào)用,我們便需要使用到緩存,而緩存相關(guān)的 Bean 是由 Spring 來(lái)管理的诗鸭。所以我們現(xiàn)在改造我們的 AOP 方法染簇,讓所有的 MethodAspectProcessor 都交給 Spring 管理。首先我們修改各個(gè) 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;

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

    /**
     * 切入被 @MethodAspectAnno 注解的方法
     *
     * @param point 連接點(diǎn)
     * @param anno  注解
     * @return 方法返回值
     * @throws Throwable 可能拋出的異常
     */
    @Around("methodPointcut() && @annotation(anno)")
    public Object doAround(ProceedingJoinPoint point, MethodAspectAnno anno) throws Throwable {
        // 通過(guò)注解獲取處理器
        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;
    }
}

本文最終方案的代碼可見(jiàn):aop-method

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蝌箍,隨后出現(xiàn)的幾起案子青灼,更是在濱河造成了極大的恐慌,老刑警劉巖妓盲,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杂拨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡悯衬,警方通過(guò)查閱死者的電腦和手機(jī)弹沽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)筋粗,“玉大人策橘,你說(shuō)我怎么就攤上這事∧纫冢” “怎么了丽已?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)买决。 經(jīng)常有香客問(wèn)我沛婴,道長(zhǎng),這世上最難降的妖魔是什么督赤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任嘁灯,我火速辦了婚禮,結(jié)果婚禮上够挂,老公的妹妹穿的比我還像新娘旁仿。我一直安慰自己,他們只是感情好孽糖,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布枯冈。 她就那樣靜靜地躺著,像睡著了一般办悟。 火紅的嫁衣襯著肌膚如雪尘奏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天病蛉,我揣著相機(jī)與錄音炫加,去河邊找鬼瑰煎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俗孝,可吹牛的內(nèi)容都是我干的酒甸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赋铝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼插勤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起革骨,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤农尖,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后良哲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體盛卡,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年筑凫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滑沧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漏健,死狀恐怖嚎货,靈堂內(nèi)的尸體忽然破棺而出橘霎,到底是詐尸還是另有隱情蔫浆,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布姐叁,位于F島的核電站瓦盛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏外潜。R本人自食惡果不足惜原环,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望处窥。 院中可真熱鬧嘱吗,春花似錦、人聲如沸滔驾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)哆致。三九已至绕德,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摊阀,已是汗流浹背耻蛇。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工踪蹬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臣咖。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓跃捣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親夺蛇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枝缔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 本章內(nèi)容: 面向切面編程的基本原理 通過(guò)POJO創(chuàng)建切面 使用@AspectJ注解 為AspectJ切面注入依賴(lài) ...
    謝隨安閱讀 3,125評(píng)論 0 9
  • Spring致力于提供一種方法管理你的業(yè)務(wù)對(duì)象。在大量Java EE的應(yīng)用中蚊惯,隨處可見(jiàn)Spring愿卸。今天我將簡(jiǎn)單的...
    JAVA架構(gòu)師的圈子閱讀 1,315評(píng)論 0 16
  • AOP實(shí)現(xiàn)可分為兩類(lèi)(按AOP框架修改源代碼的時(shí)機(jī)): 靜態(tài)AOP實(shí)現(xiàn):AOP框架在編譯階段對(duì)程序進(jìn)行修改趴荸,即實(shí)現(xiàn)...
    數(shù)獨(dú)題閱讀 2,300評(píng)論 0 22
  • 1,CBRE世邦魏理仕最新發(fā)布的《2019全球生活報(bào)告:城市指南》顯示宦焦,香港蟬聯(lián)“全球房?jī)r(jià)最高城市”榜首发钝,而上海(...
    AA面朝小溪閱讀 81評(píng)論 0 0
  • 深夜睡得最香時(shí)候 聽(tīng)到遠(yuǎn)處傳來(lái)雷聲 被該死雷聲驚醒 時(shí)時(shí)難入睡 看到閃電? 知道家鄉(xiāng)方向正在下雨 想家 想家中 ...
    創(chuàng)作文學(xué)天空閱讀 220評(píng)論 0 1