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通用操作日志组件(代码分析篇)的主要内容,如果未能解决你的问题,请参考以下文章