Bootstrap

AOP在业务中的简单使用

背景

业务组有一些给开发用的后门接口,为了做到调用溯源,业务组最近需要记录所有接口的访问记录,暂时只需要记录接口的响应结果,如果调用失败,则记录异常信息。由于后门接口较多以及只是业务组内部轻度使用,因此使用了切面的方式实现。

方案

@EnableAspectJAutoProxy
@Aspect
@Component
@Slf4j
public class ResponseLogAspect {

    @Resource
    private CommonConstants commonConstants;

    @Pointcut("@annotation(*.log.ResponseLog)")
    public void logPointcut() {

    }

    /**
     * 执行成功打印
     */
    @AfterReturning(pointcut = "logPointcut()", returning = "result")
    public void log(JoinPoint joinPoint, Object result) {
        try {
            // 降级开关
            if (!commonConstants.getBoolean("interface.response.log.switch", true)) {
                return;
            }

            ResponseLog annotation = findAnnotation(joinPoint, ResponseLog.class);
            String metric = metric(annotation.value(), joinPoint);
            log.info("interface success: {}, result: {}", metric, result);
        } catch (Exception e) {
            log.error("log error", e);
            QMonitor.recordOne("interface_response_log_fail");
        }
    }

    /**
     * 执行失败打印
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "error")
    public void logError(JoinPoint joinPoint, Throwable error) {
        try {
            // 降级开关
            if (!commonConstants.getBoolean("interface.response.log.switch", true)) {
                return;
            }
            ResponseLog annotation = findAnnotation(joinPoint, ResponseLog.class);
            String metric = metric(annotation.value(), joinPoint);
            log.error("interface fail: {}, error: {}", metric, error.getMessage());
        } catch (Exception e) {
            log.error("log error", e);
            QMonitor.recordOne("interface_response_log_fail");
        }
    }

    /**
     * 监控指标
     * @param specificName 具体指标名
     * @param point 切点
     * @return 指标名称
     */
    private String metric(String specificName, JoinPoint point) {
        if (StringUtils.isBlank(specificName)) {
            String clz = point.getTarget().getClass().getSimpleName();
            String mtd = point.getSignature().getName();
            return clz + "_" + mtd;
        } else {
            return specificName;
        }
    }

    /**
     * 注解查询
     * @param point 切点
     * @param annotationType 注解类型
     * @return 注解信息
     */
    private <A extends Annotation> A findAnnotation(JoinPoint point, Class<A> annotationType) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        return AnnotationUtils.findAnnotation(signature.getMethod(), annotationType);
    }
}

接下来只需要在后门接口上增加对应的注解即可:

    @RequestMapping(value = "save", method = RequestMethod.POST)
    @ResponseBody
    @ResponseLog("/voucher/save")
    public APIResponse<Boolean> save(HttpServletRequest request, @RequestBody VoucherCommit voucherCommit) {
       // 代金券保存接口
   }

加餐

  1. @Target({ElementType.METHOD}):指定该注解可以应用于方法。如果不加这个注解,则表示默认该注解可以应用到类与方法上,但是加上后就表示这个注解只能作用于方法,否则会报错。
  2. springboot项目由于存在spring-boot-autoconfigure依赖,会默认开启aop代理,所以注解@EnableAspectJAutoProxy可以不用加,但是由于可以在配置文件中修改默认开启的逻辑,所以建议加上避免失效。
  3. @Pointcut注解中的参数:@within和@annotation。@annotation注解用于匹配那些具有指定注解的方法,@within注解用于匹配那些具有指定注解的类中的所有方法,即使这些方法本身没有显式地标注注解。
    // 切点:匹配带有@OnlyIntranetAccess注解的类
    @Pointcut("@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)")
    public void onlyIntranetAccessOnClass() {}

    // 切点:匹配带有@OnlyIntranetAccess注解的方法
    @Pointcut("@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)")
    public void onlyIntranetAccessOnMethed() {}

;