4. 代码实现解析
4.1 代码结构
上面的操作日志主要是通过一个 AOP 拦截器实现的,整体主要分为 AOP 模块、日志解析模块、日志保存模块、Starter 模块;组件提供了4个扩展点,分别是:自定义函数、默认处理人、业务保存和查询;业务可以根据自己的业务特性定制符合自己业务的逻辑。
4.2 模块介绍
有了上面的分析,已经得出一种我们期望的操作日志记录的方式,接下来我们看下如何实现上面的逻辑。实现主要分为下面几个步骤:
- AOP 拦截逻辑
- 解析逻辑
- 模板解析
- LogContext 逻辑
- 默认的 operator 逻辑
- 自定义函数逻辑
- 默认的日志持久化逻辑
- Starter 封装逻辑
4.2.1 AOP 拦截逻辑
这块逻辑主要是一个拦截器,针对 @LogRecord 注解分析出需要记录的操作日志,然后把操作日志持久化,这里把注解命名为 @LogRecordAnnotation。接下来,我们看下注解的定义:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface LogRecordAnnotation { String success(); String fail() default ""; String operator() default ""; String bizNo(); String category() default ""; String detail() default ""; String condition() default ""; }
注解中除了上面提到参数外,还增加了 fail、category、detail、condition 等参数,这几个参数是为了满足特定的场景,后面还会给出具体的例子。
为了保持简单,组件的必填参数就两个。业务中的 AOP 逻辑大部分是使用 @Aspect 注解实现的,但是基于注解的 AOP 在 Spring boot 1.5 中兼容性是有问题的,组件为了兼容 Spring boot1.5 的版本我们手工实现 Spring 的 AOP 逻辑。切面选择 AbstractBeanFactoryPointcutAdvisor
实现,切点是通过 StaticMethodMatcherPointcut
匹配包含 LogRecordAnnotation
注解的方法。通过实现 MethodInterceptor
接口实现操作日志的增强逻辑。下面是拦截器的切点逻辑:
public class LogRecordPointcut extends StaticMethodMatcherPointcut implements Serializable { // LogRecord的解析类 private LogRecordOperationSource logRecordOperationSource; @Override public boolean matches(@NonNull Method method, @NonNull Class<?> targetClass) { // 解析 这个 method 上有没有 @LogRecordAnnotation 注解,有的话会解析出来注解上的各个参数 return !CollectionUtils.isEmpty(logRecordOperationSource.computeLogRecordOperations(method, targetClass)); } void setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) { this.logRecordOperationSource = logRecordOperationSource; } }
切面的增强逻辑主要代码如下:
@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 { Class<?> targetClass = getTargetClass(target); Object ret = null; MethodExecuteResult methodExecuteResult = new MethodExecuteResult(true, null, ""); 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); } try { ret = invoker.proceed(); } catch (Exception e) { methodExecuteResult = new MethodExecuteResult(false, e, e.getMessage()); } try { if (!CollectionUtils.isEmpty(operations)) { recordExecute(ret, method, args, operations, targetClass, methodExecuteResult.isSuccess(), methodExecuteResult.getErrorMsg(), functionNameAndReturnMap); } } catch (Exception t) { //记录日志错误不要影响业务 log.error("log record parse exception", t); } finally { LogRecordContext.clear(); } if (methodExecuteResult.throwable != null) { throw methodExecuteResult.throwable; } return ret; }
拦截逻辑的流程:
可以看到,操作日志的记录持久化是在方法执行完之后执行的,当方法抛出异常之后会先捕获异常,等操作日志持久化完成后再抛出异常。在业务的方法执行之前,会对提前解析的自定义函数求值,解决了前面提到的需要查询修改之前的内容。