bizlog通用操作日志组件(代码分析篇)
创始人
2024-02-05 05:30:59

引言

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

代码结构

在这里插入图片描述
该组件主要是通过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;@Overridepublic 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 operations = new ArrayList<>();Map functionNameAndReturnMap = new HashMap<>();try {operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass);List 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 expressionCache = new ConcurrentHashMap<>(64);private final Map 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 variables = LogRecordContext.getVariables();if (variables != null && variables.size() > 0) {for (Map.Entry 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 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 allFunctionMap;public ParseFunctionFactory(List 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;}@Overridepublic String apply(String functionName, Object value) {IParseFunction function = parseFunctionFactory.getFunction(functionName);if (function == null) {return value.toString();}return function.apply(value);}@Overridepublic 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.* 

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);}@Beanpublic ParseFunctionFactory parseFunctionFactory(@Autowired List 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);
//    }@Beanpublic 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();}@Overridepublic 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");}}
}

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

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

总结

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

相关内容

热门资讯

北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...