• bizlog通用操作日志组件(代码分析篇)


    引言

    上篇博客中介绍了通用操作日志组件的使用方法,本篇博客将从源码出发,学习一下该组件是如何实现的。

    代码结构

    在这里插入图片描述
    该组件主要是通过AOP拦截器实现的,整体上可分为四个模块:AOP模块、日志解析模块、日志保存模块、Starter模块;另外,提供了四个扩展点:自定义函数、默认处理人、业务保存和查询。

    模块介绍

    AOP拦截

    1. 针对@LogRecord注解分析日志,自定义注解如下:

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface LogRecord {
        /**
         * @return 方法执行成功后的日志模版
         */
        String success();
    
        /**
         * @return 方法执行失败后的日志模版
         */
        String fail() default "";
    
        /**
         * @return 日志的操作人
         */
        String operator() default "";
    
        /**
         * @return 操作日志的类型,比如:订单类型、商品类型
         */
        String type();
    
        /**
         * @return 日志的子类型,比如订单的C端日志,和订单的B端日志,type都是订单类型,但是子类型不一样
         */
        String subType() default "";
    
        /**
         * @return 日志绑定的业务标识
         */
        String bizNo();
    
        /**
         * @return 日志的额外信息
         */
        String extra() default "";
    
        /**
         * @return 是否记录日志
         */
        String condition() default "";
    
        /**
         * 记录成功日志的条件
         *
         * @return 表示成功的表达式,默认为空,代表不抛异常为成功
         */
        String successCondition() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    注解的参数在上篇博客的使用中基本都有提到,这里就不再赘述了。

    2. 切点通过StaticMethodMatcherPointcut匹配包含LogRecord注解的方法

    public class LogRecordPointcut extends StaticMethodMatcherPointcut implements Serializable {
    
        //LogRecord解析类
        private LogRecordOperationSource logRecordOperationSource;
    
        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            // 解析 这个 method 上有没有 @LogRecord 注解,有的话会解析出来注解上的各个参数
            return !CollectionUtils.isEmpty(logRecordOperationSource.computeLogRecordOperations(method, targetClass));
        }
    
        void setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) {
            this.logRecordOperationSource = logRecordOperationSource;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. 通过实现MethodInterceptor接口实现操作日志的切面增强逻辑

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        //记录日志
        return execute(invocation, invocation.getThis(), method, invocation.getArguments());
    }
    
    private Object execute(MethodInvocation invoker, Object target, Method method, Object[] args) throws Throwable {
        //代理不拦截
        if (AopUtils.isAopProxy(target)) {
            return invoker.proceed();
        }
        StopWatch stopWatch = new StopWatch(MONITOR_NAME);
        stopWatch.start(MONITOR_TASK_BEFORE_EXECUTE);
        Class<?> targetClass = getTargetClass(target);
        Object ret = null;
        MethodExecuteResult methodExecuteResult = new MethodExecuteResult(method, args, targetClass);
        LogRecordContext.putEmptySpan();
        Collection<LogRecordOps> operations = new ArrayList<>();
        Map<String, String> functionNameAndReturnMap = new HashMap<>();
        try {
            operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass);
            List<String> spElTemplates = getBeforeExecuteFunctionTemplate(operations);
            functionNameAndReturnMap = processBeforeExecuteFunctionTemplate(spElTemplates, targetClass, method, args);
        } catch (Exception e) {
            log.error("log record parse before function exception", e);
        } finally {
            stopWatch.stop();
        }
    
        try {
            ret = invoker.proceed();
            methodExecuteResult.setResult(ret);
            methodExecuteResult.setSuccess(true);
        } catch (Exception e) {
            methodExecuteResult.setSuccess(false);
            methodExecuteResult.setThrowable(e);
            methodExecuteResult.setErrorMsg(e.getMessage());
        }
        stopWatch.start(MONITOR_TASK_AFTER_EXECUTE);
        try {
            if (!CollectionUtils.isEmpty(operations)) {
                recordExecute(methodExecuteResult, functionNameAndReturnMap, operations);
            }
        } catch (Exception t) {
            log.error("log record parse exception", t);
            throw t;
        } finally {
            LogRecordContext.clear();
            stopWatch.stop();
            try {
                logRecordPerformanceMonitor.print(stopWatch);
            } catch (Exception e) {
                log.error("execute exception", e);
            }
        }
    
        if (methodExecuteResult.getThrowable() != null) {
            throw methodExecuteResult.getThrowable();
        }
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    解析逻辑

    解析核心类是LogRecordExpressionEvaluator,解析Spring EL表达式

    public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator {
    
        private Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
    
        private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
    
        public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
            return getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    expressionCache这个Map是为了缓存方法、表达式和 SpEL 的 Expression 的对应关系,让方法注解上添加的 SpEL 表达式只解析一次。targetMethodCache Map是为了缓存传入到 Expression 表达式的 Object。

    getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class)这行代码就是解析参数和变量的。

    日志上下文实现

    方法参数中不存在的变量,我们可以通过LogRecordContext传入,而通过LogRecordContext传入的变量也是使用SpEL的getValue方法取值的。

    1. 在LogRecordValueParser中创建EvaluationContext

    
    EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, ret, errorMsg, beanFactory);
    
    
    public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass,
                                                     Object result, String errorMsg, BeanFactory beanFactory) {
        Method targetMethod = getTargetMethod(targetClass, method);
        LogRecordEvaluationContext evaluationContext = new LogRecordEvaluationContext(
                null, targetMethod, args, getParameterNameDiscoverer(), result, errorMsg);
        if (beanFactory != null) {
            evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
        }
        return evaluationContext;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在解析的时候调用 getValue 方法传入的参数 evalContext,就是上面这个 EvaluationContext 对象。

    2. LogRecordEvaluationContext

    LogRecordEvaluationContext中将方法的参数、LogRecordContext中的变量、方法的返回值和ErrorMsg都放到SpEL解析的RootObject中。

    public class LogRecordEvaluationContext extends MethodBasedEvaluationContext {
    
        public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments,
                                          ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) {
           //把方法的参数都放到 SpEL 解析的 RootObject 中
           super(rootObject, method, arguments, parameterNameDiscoverer);
           //把 LogRecordContext 中的变量都放到 RootObject 中
            Map<String, Object> variables = LogRecordContext.getVariables();
            if (variables != null && variables.size() > 0) {
                for (Map.Entry<String, Object> entry : variables.entrySet()) {
                    setVariable(entry.getKey(), entry.getValue());
                }
            }
            //把方法的返回值和 ErrorMsg 都放到 RootObject 中
            setVariable("_ret", ret);
            setVariable("_errorMsg", errorMsg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    默认操作人逻辑

    在 LogRecordInterceptor 中 IOperatorGetService 接口,这个接口可以获取到当前的用户。组件在解析operator的时候,就判断注解上的operator是否是空,为空会查询默认用户。

    private String getOperatorIdFromServiceAndPutTemplate(LogRecordOps operation, List<String> spElTemplates) {
    
        String realOperatorId = "";
        if (StringUtils.isEmpty(operation.getOperatorId())) {
            realOperatorId = operatorGetService.getUser().getOperatorId();
            if (StringUtils.isEmpty(realOperatorId)) {
                throw new IllegalArgumentException("[LogRecord] operator is null");
            }
        } else {
            spElTemplates.add(operation.getOperatorId());
        }
        return realOperatorId;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    自定义函数逻辑

    1. IParseFunction的接口定义

    public interface IParseFunction {
    
        default boolean executeBefore() {
            return false;
        }
    
        String functionName();
    
        /**
         * @param value 函数入参
         * @return 文案
         * @since 1.1.0 参数从String 修改为Object类型,可以处理更多的场景,可以通过SpEL表达式传递对象了
         * 老版本需要改下自定义函数的声明,实现使用中把 用到 value的地方修改为 value.toString 就可以兼容了
         */
        String apply(Object value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    executeBefore 函数代表了自定义函数是否在业务代码执行之前解析。

    2. ParseFunctionFactory:把所有的IParseFunction注入到函数工厂中

    public class ParseFunctionFactory {
        private Map<String, IParseFunction> allFunctionMap;
    
        public ParseFunctionFactory(List<IParseFunction> parseFunctions) {
            if (CollectionUtils.isEmpty(parseFunctions)) {
                return;
            }
            allFunctionMap = new HashMap<>();
            for (IParseFunction parseFunction : parseFunctions) {
                if (StringUtils.isEmpty(parseFunction.functionName())) {
                    continue;
                }
                allFunctionMap.put(parseFunction.functionName(), parseFunction);
            }
        }
    
        public IParseFunction getFunction(String functionName) {
            return allFunctionMap.get(functionName);
        }
    
        public boolean isBeforeFunction(String functionName) {
            return allFunctionMap.get(functionName) != null && allFunctionMap.get(functionName).executeBefore();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3. DefaultFunctionServiceImpl:根据传入的函数名称 functionName 找到对应的 IParseFunction,然后把参数传入到 IParseFunction 的 apply 方法上最后返回函数的值。

    public class DefaultFunctionServiceImpl implements IFunctionService {
    
        private final ParseFunctionFactory parseFunctionFactory;
    
        public DefaultFunctionServiceImpl(ParseFunctionFactory parseFunctionFactory) {
            this.parseFunctionFactory = parseFunctionFactory;
        }
    
        @Override
        public String apply(String functionName, Object value) {
            IParseFunction function = parseFunctionFactory.getFunction(functionName);
            if (function == null) {
                return value.toString();
            }
            return function.apply(value);
        }
    
        @Override
        public boolean beforeFunction(String functionName) {
            return parseFunctionFactory.isBeforeFunction(functionName);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    日志持久化逻辑

    LogRecordInterceptor引用了ILogRecordService,业务可以实现这个接口保存日志。

    @Slf4j
    public class DefaultLogRecordServiceImpl implements ILogRecordService {
    
    //    @Resource
    //    private LogRecordMapper logRecordMapper;
    
        @Override
    //    @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void record(LogRecord logRecord) {
            log.info("【logRecord】log={}", logRecord);
            //throw new RuntimeException("sss");
    //        logRecordMapper.insertSelective(logRecord);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    业务可以把保存设置成异步或者同步,可以和业务放在一个事务中保证操作日志和业务的一致性,也可以新开辟一个事务,保证日志的错误不影响业务的事务。业务可以保存在 Elasticsearch、数据库或者文件中,用户可以根据日志结构和日志的存储实现相应的查询逻辑。

    Starter逻辑封装

    我们直接在Spring Boot启动类上添加@EnableLogRecord注解即可使用,就是对上面实现逻辑的组件做了Starter封装。

    1. EnableLogRecord注解

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(LogRecordConfigureSelector.class)
    public @interface EnableLogRecord {
    
        String tenant();
    
        /**
         * !不要删掉,为 null 就不代理了哦
         * true 都使用 CGLIB 代理
         * false 目标对象实现了接口 – 使用JDK动态代理机制(代理所有实现了的接口) 目标对象没有接口(只有实现类) – 使用CGLIB代理机制
         *
         * @return 不强制 cglib
         */
        boolean proxyTargetClass() default false;
    
        /**
         * Indicate how caching advice should be applied. The default is
         * {@link AdviceMode#PROXY}.
         *
         * @return 代理方式
         * @see AdviceMode
         */
        AdviceMode mode() default AdviceMode.PROXY;
    
        /**
         * 记录日志日志与业务日志是否同一个事务
         *
         * @return 默认独立
         */
        boolean joinTransaction() default false;
    
        /**
         * Indicate the ordering of the execution of the transaction advisor
         * when multiple advices are applied at a specific joinpoint.
         * 

    The default is {@link Ordered#LOWEST_PRECEDENCE}. * * @return 事务 advisor 的优先级 */ int order() default Ordered.LOWEST_PRECEDENCE; }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    代码中Import了LogRecordConfigureSelector.class,在 LogRecordConfigureSelector 类中暴露了 LogRecordProxyAutoConfiguration 类。

    2. 核心类LogRecordProxyAutoConfiguration装配上面组件

    @Configuration
    @EnableConfigurationProperties({LogRecordProperties.class})
    @Slf4j
    public class LogRecordProxyAutoConfiguration implements ImportAware {
    
        private AnnotationAttributes enableLogRecord;
    
    
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public LogRecordOperationSource logRecordOperationSource() {
            return new LogRecordOperationSource();
        }
    
        @Bean
        @ConditionalOnMissingBean(IFunctionService.class)
        public IFunctionService functionService(ParseFunctionFactory parseFunctionFactory) {
            return new DefaultFunctionServiceImpl(parseFunctionFactory);
        }
    
        @Bean
        public ParseFunctionFactory parseFunctionFactory(@Autowired List<IParseFunction> parseFunctions) {
            return new ParseFunctionFactory(parseFunctions);
        }
    
        @Bean
        @ConditionalOnMissingBean(IParseFunction.class)
        public DefaultParseFunction parseFunction() {
            return new DefaultParseFunction();
        }
    
    
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public BeanFactoryLogRecordAdvisor logRecordAdvisor() {
            BeanFactoryLogRecordAdvisor advisor =
                    new BeanFactoryLogRecordAdvisor();
            advisor.setLogRecordOperationSource(logRecordOperationSource());
            advisor.setAdvice(logRecordInterceptor());
            advisor.setOrder(enableLogRecord.getNumber("order"));
            return advisor;
        }
    
        @Bean
        @ConditionalOnMissingBean(ILogRecordPerformanceMonitor.class)
        public ILogRecordPerformanceMonitor logRecordPerformanceMonitor() {
            return new DefaultLogRecordPerformanceMonitor();
        }
    
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public LogRecordInterceptor logRecordInterceptor() {
            LogRecordInterceptor interceptor = new LogRecordInterceptor();
            interceptor.setLogRecordOperationSource(logRecordOperationSource());
            interceptor.setTenant(enableLogRecord.getString("tenant"));
            interceptor.setJoinTransaction(enableLogRecord.getBoolean("joinTransaction"));
            //interceptor.setLogFunctionParser(logFunctionParser(functionService));
            //interceptor.setDiffParseFunction(diffParseFunction);
            interceptor.setLogRecordPerformanceMonitor(logRecordPerformanceMonitor());
            return interceptor;
        }
    
    //    @Bean
    //    public LogFunctionParser logFunctionParser(IFunctionService functionService) {
    //        return new LogFunctionParser(functionService);
    //    }
    
        @Bean
        public DiffParseFunction diffParseFunction(IDiffItemsToLogContentService diffItemsToLogContentService) {
            DiffParseFunction diffParseFunction = new DiffParseFunction();
            diffParseFunction.setDiffItemsToLogContentService(diffItemsToLogContentService);
            return diffParseFunction;
        }
    
        @Bean
        @ConditionalOnMissingBean(IDiffItemsToLogContentService.class)
        @Role(BeanDefinition.ROLE_APPLICATION)
        public IDiffItemsToLogContentService diffItemsToLogContentService(LogRecordProperties logRecordProperties) {
            return new DefaultDiffItemsToLogContentService(logRecordProperties);
        }
    
        @Bean
        @ConditionalOnMissingBean(IOperatorGetService.class)
        @Role(BeanDefinition.ROLE_APPLICATION)
        public IOperatorGetService operatorGetService() {
            return new DefaultOperatorGetServiceImpl();
        }
    
        @Bean
        @ConditionalOnMissingBean(ILogRecordService.class)
        @Role(BeanDefinition.ROLE_APPLICATION)
        public ILogRecordService recordService() {
            return new DefaultLogRecordServiceImpl();
        }
    
        @Override
        public void setImportMetadata(AnnotationMetadata importMetadata) {
            this.enableLogRecord = AnnotationAttributes.fromMap(
                    importMetadata.getAnnotationAttributes(EnableLogRecord.class.getName(), false));
            if (this.enableLogRecord == null) {
                log.info("EnableLogRecord is not present on importing class");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    这个类继承 ImportAware 是为了拿到 EnableLogRecord 上的租户属性,这个类使用变量 logRecordAdvisor 和 logRecordInterceptor 装配了 AOP,同时把自定义函数注入到了 logRecordAdvisor 中。

    对外扩展类:分别是IOperatorGetService、ILogRecordService、IParseFunction。业务可以自己实现相应的接口,因为配置了 @ConditionalOnMissingBean,所以用户的实现类会覆盖组件内的默认实现。

    总结

    通过对bizlog组件的使用和代码分析,很好地解决了项目的需求,也了解了其内部是如何实现的,算是有所收获。

  • 相关阅读:
    美欧之后,台湾地区推出芯片法案:搞研发、买设备最高抵减50%税额
    为中小企业的网络推广策略解析:扩大品牌知名度和曝光度
    Rust9.1 Object-Oriented Programming Features of Rust
    【多线程】线程安全与线程同步
    el-cascader 最后一级不显示出来
    vue组件传值
    JS+CSS随机点名详细介绍复制可用(可自己添加人名)
    用html、css和jQuery实现图片翻页的特效
    网络工程师的网络安全之路:应对威胁与保障数据
    解决 This request has been blocked; this endpoint must be available over WSS.
  • 原文地址:https://blog.csdn.net/u013034223/article/details/127933368