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

Posted 好好生活_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 "";

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

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;
    

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;

解析逻辑

解析核心类是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);
    

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;

在解析的时候调用 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);
    

默认操作人逻辑

在 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. 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);

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();
    

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

日志持久化逻辑

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

业务可以把保存设置成异步或者同步,可以和业务放在一个事务中保证操作日志和业务的一致性,也可以新开辟一个事务,保证日志的错误不影响业务的事务。业务可以保存在 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.
     * <p>The default is @link Ordered#LOWEST_PRECEDENCE.
     *
     * @return 事务 advisor 的优先级
     */
    int order() default Ordered.LOWEST_PRECEDENCE;

代码中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(logRecordIntercep

以上是关于bizlog通用操作日志组件(代码分析篇)的主要内容,如果未能解决你的问题,请参考以下文章

bizlog通用操作日志组件(使用篇)

bizlog通用操作日志组件(使用篇)

bizlog通用操作日志组件(使用篇)

开源:如何优雅的实现一个操作日志组件

开源:如何优雅的实现一个操作日志组件

Loki日志收集单进程模式部署