在上篇博客中介绍了通用操作日志组件的使用方法,本篇博客将从源码出发,学习一下该组件是如何实现的。
该组件主要是通过AOP拦截器实现的,整体上可分为四个模块:AOP模块、日志解析模块、日志保存模块、Starter模块;另外,提供了四个扩展点:自定义函数、默认处理人、业务保存和查询。
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、数据库或者文件中,用户可以根据日志结构和日志的存储实现相应的查询逻辑。
我们直接在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组件的使用和代码分析,很好地解决了项目的需求,也了解了其内部是如何实现的,算是有所收获。