引言
在上篇博客中介绍了通用操作日志组件的使用方法,本篇博客将从源码出发,学习一下该组件是如何实现的。
代码结构
该组件主要是通过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(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");
}
}
}
这个类继承 ImportAware 是为了拿到 EnableLogRecord 上的租户属性,这个类使用变量 logRecordAdvisor 和 logRecordInterceptor 装配了 AOP,同时把自定义函数注入到了 logRecordAdvisor 中。
对外扩展类:分别是IOperatorGetService、ILogRecordService、IParseFunction。业务可以自己实现相应的接口,因为配置了 @ConditionalOnMissingBean,所以用户的实现类会覆盖组件内的默认实现。
总结
通过对bizlog组件的使用和代码分析,很好地解决了项目的需求,也了解了其内部是如何实现的,算是有所收获。