使用Aop实现自定义注解 - 实战篇(统一日志打印)

使用Aop实现自定义注解- 原理篇
使用Aop实现自定义注解 - 实战篇(统一日志打印)

统一日志打印

约定大于配置,系统提供对外的接口要有以下要求,这样的好处是我们可以对接口统一做定制化日志管理。比如只对更新接口打印入参。

  1. 接口命名
    1. 单个查询接口,? findById
    2. 批量查询接口,? listByIds
    3. 单个更新接口,? updateById
    4. 批量更新接口,? batchUpdateById
    5. 单个新增接口,? save
    6. 批量新增接口,? batchSave

枚举定义

@Documented
@Inherited
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface TraceLogAnnotation {
    /**
     * 是否打印详细结果,有的结果非常大,影响性能
     */
    boolean isNeedPrintResult() default true;

    /**
     * 是否打印入参,有的参数非常大,影响性能
     */
    boolean isNeedPrintParam() default true;

    /**
     * 是否只在正常情况下打印
     */
    boolean isNeedPrintNormal() default false;


}

切面AOP实现

AOP实现,对所有对外提供服务的接口提供了默认实现:

  • 默认扫描所有对外接口,如果发生异常,就会打印异常日志和traceID。
  • 对于非查询类接口,还要打印出入参,作为操作流水。
  • 支持使用注解对个别接口单独配置,整体优先级: 注解属性 > 默认属性
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class TraceLogAspect {

    private static final org.slf4j.Logger LOGGER_RT = org.slf4j.LoggerFactory.getLogger("LOGGER_RT");
    /**
     * 默认参数
     */
    private static final boolean IS_NEED_PRINT_NORMAL = false;
    private static final boolean IS_NEED_PRINT_PARAM = true;
    private static final boolean IS_NEED_PRINT_RESULT = true;
    /**
     * 配置参数
     * 默认打印对外实现类bundle的非查询方法
     */
    private static final String[] PRINT_CLASS_CONDITION = {"bundle"};
    private static final String[] PRINT_METHOD_CONDITION = {"save", "update", "remove", "enable", "disable", "add", "delete", "fix"};
    private static final String[] DEFAULT_SKIP_CONDITION = {"list", "find", "get", "page", "init", "import", "excel"};


    /**
     * traceLog 切点表达式(覆盖所有对外接口) 参考https://www.baeldung.com/spring-aop-pointcut-tutorial
     */
    @Around(value = "within(com.baidu.base.api.bundle..*Bundle)")
    public Object aroundAllOutApi(ProceedingJoinPoint joinPoint) throws Throwable {
        //此处切点获取不到注解参数
        if (needPrintParam(joinPoint)) {
            return around(joinPoint, true, true, true);
        } else {
            return around(joinPoint, null, null, null);
        }
    }

    /**
     * 判断是否需要打印入参
     *
     * @param joinPoint 切点
     * @return true: 接口需要打印入参
     */
    private boolean needPrintParam(ProceedingJoinPoint joinPoint) {
        String className = joinPoint.getSignature().getDeclaringType().getSimpleName().toLowerCase();
        for (String printClassCondition : PRINT_CLASS_CONDITION) {
            if (className.contains(printClassCondition)) {

                String methodName = joinPoint.getSignature().getName().toLowerCase();
                for (String printMethodCondition : PRINT_METHOD_CONDITION) {
                    if (methodName.contains(printMethodCondition)) {
                        return true;
                    }
                }

            }
        }
        return false;
    }

    /**
     * 覆盖使用注解的方法
     */
    @Around(value = "@within(com.baidu.base.api.annotation.TraceLogAnnotation)")
    public Object aroundAnnotationApi(ProceedingJoinPoint joinPoint) throws Throwable {
        return around(joinPoint, null, null, null);
    }

    /**
     * 打印参数优先级: 函数入参 》 joinPoint 》 默认值
     *
     * @param joinPoint 切入点
     */
    private Object around(ProceedingJoinPoint joinPoint, Boolean needPrintNormal, Boolean needPrintParam, Boolean needPrintResult) throws Throwable {
        MethodSignature sign = (MethodSignature) joinPoint.getSignature();
        Method method = sign.getMethod();
        String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        Stopwatch stopwatch = Stopwatch.createStarted();
        Object[] args = joinPoint.getArgs();
        TraceLogAnnotation annotation = method.getAnnotation(TraceLogAnnotation.class);

        //获取打印参数配置
        boolean isNeedPrintNormal = needPrintNormal != null ? needPrintNormal : (annotation != null ? annotation.isNeedPrintNormal() : IS_NEED_PRINT_NORMAL);
        boolean isNeedPrintParam = needPrintParam != null ? needPrintParam : (annotation != null ? annotation.isNeedPrintParam() : IS_NEED_PRINT_PARAM);
        boolean isNeedPrintResult = needPrintResult != null ? needPrintResult : (annotation != null ? annotation.isNeedPrintResult() : IS_NEED_PRINT_RESULT);

        Object result = null;
        try {
            result = joinPoint.proceed(args);
            return result;
        } catch (Exception e) {
            if (!isNeedPrintParam) {
                LOGGER_RT.error("className: {},methodName: {},result: {},time: {}",  className,
                        methodName, JSON.toJSONString(result), stopwatch.elapsed(TimeUnit.MILLISECONDS), e);
            } else if (!isNeedPrintResult) {
                LOGGER_RT.error("className: {},methodName: {},args: {},time: {}", className,
                        methodName, JSON.toJSONString(args), stopwatch.elapsed(TimeUnit.MILLISECONDS), e);
            } else {
                LOGGER_RT.error("className: {},methodName: {},args: {},result: {},time: {}", 
                        className, methodName, JSON.toJSONString(args), JSON.toJSONString(result), stopwatch.elapsed(TimeUnit.MILLISECONDS), e);
            }
            throw e;
        } finally {
            if (isNeedPrintNormal) {
                if (!isNeedPrintParam) {
                    LOGGER_RT.info("className: {},methodName: {},result: {},time: {}", 
                            className, methodName, JSON.toJSONString(result), stopwatch.elapsed(TimeUnit.MILLISECONDS));
                } else if (!isNeedPrintResult) {
                    LOGGER_RT.info("className: {},methodName: {},args: {},time: {}", 
                            className, methodName, JSON.toJSONString(args), stopwatch.elapsed(TimeUnit.MILLISECONDS));
                } else {
                    LOGGER_RT.info("className: {},methodName: {},args: {},result: {},time: {}", 
                            className, methodName, JSON.toJSONString(args), JSON.toJSONString(result), stopwatch.elapsed(TimeUnit.MILLISECONDS));
                }
            }
        }
    }
}

后续优化方向

如果平台继续做大,QPS过高时,Spring这种AOP 性能会有瓶颈,到时候可以考虑改为在代码编译期将切面代码织入。通过 Java 的 APT(Annotation Process Tool)在编译期间直接向 Method 里面植入切面代码实现编译时 AOP,这样就不会有性能损耗问题。


版权声明:本文为h2453532874原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。