基于 AOP 抽离方法的重复代码

背景

今天师兄和我说,“之叶,你设计一个方案,把目前业务方法中和业务无关的逻辑都抽离出来,让每个方法只关心自己的业务逻辑”。我会心一笑 👇(因为我们早应该做这件事情了)

邪魅一笑

现有的业务方法

之前代码里每个业务方法几乎都是长这样:

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;
    }
  
    // 后面还有若干个类似的业务方法
    ......
}

很容易可以看出,记录方法开始时间捕获异常并处理打印错误日志记录方法耗时 这些都是和业务没有关系的,业务方法关心的,只应该是 业务逻辑代码 才对。一两个方法这个样子看起来也还好,但是目前项目里面已经有十几个这种样子的方法了,而且以后还会更多,重复代码对我们简直不能忍 —— 是的,我也早就看这些业务方法不顺眼了,安排!

必须安排

设计方案

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:

@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);
    }
}

启动应用,看一下目前调用业务方法时的情况:

  1. 调用正常情况(a=2,b=1)

    正常情况
  2. 调用出错情况(a=2,b=0)

    错误情况

编写切面

现在的 Java Web 应用,使用注解来进行配置和做 AOP 已经是主流 —— 因为相比 XML,注解更简单而且更好用。所以我们先定义一个 @ServiceMethodAspectAnno

@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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,142评论 0 9
  • Spring致力于提供一种方法管理你的业务对象。在大量Java EE的应用中,随处可见Spring。今天我将简单的...
    JAVA架构师的圈子阅读 1,328评论 0 16
  • AOP实现可分为两类(按AOP框架修改源代码的时机): 静态AOP实现:AOP框架在编译阶段对程序进行修改,即实现...
    数独题阅读 2,317评论 0 22
  • 1,CBRE世邦魏理仕最新发布的《2019全球生活报告:城市指南》显示,香港蝉联“全球房价最高城市”榜首,而上海(...
    AA面朝小溪阅读 82评论 0 0
  • 深夜睡得最香时候 听到远处传来雷声 被该死雷声惊醒 时时难入睡 看到闪电️ 知道家乡方向正在下雨 想家 想家中 ...
    创作文学天空阅读 226评论 0 1