文章目录
- 前言
- [一、设计 / 对比 / 实现](#一、设计 / 对比 / 实现)
-
- [1.1 注解](#1.1 注解)
-
- [1.1.1 EnableBizLogger](#1.1.1 EnableBizLogger)
- [1.1.2 BizLogger](#1.1.2 BizLogger)
- [1.1.3 BizLoggerComponent](#1.1.3 BizLoggerComponent)
- [1.1.4 BizLoggerFunction](#1.1.4 BizLoggerFunction)
- [1.2 自定义函数](#1.2 自定义函数)
-
- [1.2.1 IBizLoggerFunctionRegistrar](#1.2.1 IBizLoggerFunctionRegistrar)
- [1.2.2 AbstractBizLoggerFunctionResolver](#1.2.2 AbstractBizLoggerFunctionResolver)
- [1.3 日志上下文](#1.3 日志上下文)
-
- [1.3.1 BizLoggerContext](#1.3.1 BizLoggerContext)
- [1.4 SpEL](#1.4 SpEL)
-
- [1.4.1 BizLoggerEvaluationContext](#1.4.1 BizLoggerEvaluationContext)
- [1.4.2 BizLoggerExpressionEvaluator](#1.4.2 BizLoggerExpressionEvaluator)
- [1.5 解析注解并生成日志对象](#1.5 解析注解并生成日志对象)
-
- [1.5.1 BizLogDTO](#1.5.1 BizLogDTO)
- [1.5.1 IBizLogCreatorsExecutorFactory](#1.5.1 IBizLogCreatorsExecutorFactory)
- [1.5.2 IBizLogCreatorsExecutor](#1.5.2 IBizLogCreatorsExecutor)
- [1.5.3 IBizLogCreatorFactory](#1.5.3 IBizLogCreatorFactory)
- [1.5.4 IBizLogCreator](#1.5.4 IBizLogCreator)
- [1.6 Aspect](#1.6 Aspect)
-
- [1.6.1 BizLoggerAspect](#1.6.1 BizLoggerAspect)
- [1.7 Configuration](#1.7 Configuration)
-
- [1.7.1 BizLoggerConfigureSelector](#1.7.1 BizLoggerConfigureSelector)
- [1.7.2 BizLoggerAutoConfiguration](#1.7.2 BizLoggerAutoConfiguration)
- 二、其他杂类
-
- [2.1 services](#2.1 services)
-
- [2.1.1 IBizLoggerPerformanceMonitor](#2.1.1 IBizLoggerPerformanceMonitor)
- [2.1.2 IBizLogOperatorService](#2.1.2 IBizLogOperatorService)
- [2.1.3 IBizLogSyncService](#2.1.3 IBizLogSyncService)
- [2.1.4 BizLoggerProperties](#2.1.4 BizLoggerProperties)
前言
美团技术团队曾经发过一篇 如何优雅地记录操作日志?, 简单介绍了业务中为了记录 Who did what at what time
的需求实现, 通过AOP和注解的方式与业务解耦. 遂拜读了下项目源码 mzt-biz-log 和另一个人的一个差异化实现 log-record.
一直以来就想让司内服务有一套清晰的操作日志, 方便业务人员分析客户动态等等. 加上最近这几天其他人负责的服务在出现某些问题需要溯源的时候, 发现其业务操作日志既不可读, 又跟一堆系统日志混在一起. 导致想找到问题原因的时候必须依赖负责服务的开发人员(因为就他知道这是啥, 也许是一句sql, 也许是一行不明所以的log之类的), 十分痛苦.
看过上述的两个项目后, 发现二者其中的代码编写也好, 设计也罢, 让我感觉稍微有点乱, 于是乎带着学习大厂研发的设计的想法, 趁此机会, 自己实现一遍框架功能, 目的并不是简单的把代码抄一遍, 本文将在部分章节具体说明三个框架设计的差异, 优缺点等等.
没时间画图, 直接上代码, 环境: JDK21 + SpringBoot 3.2.1.
一、设计 / 对比 / 实现
包括上述两个框架, 简单来说, 就是以AOP + 注解为基础, 辅以SpEL解析和ThreadLocal的上下文能力, 完成操作日志的配置化, 解耦化.
- mzt-biz-log 考虑兼容性, 选择自行构建并配置AOP组件
- log-record 直接使用注解配置Advice的方式
因为主要用于司内的服务, 所以我也选择直接使用注解配置的方式开启AOP能力
1.1 注解
- EnableBizLogger: 框架自动配置的开关
- BizLoggers: Repeatable, 配置多个BizLogger
- BizLogger: 标记方法, 配置成功日志, 失败日志等, 是记录操作日志的入口
- BizLoggerComponent: 标记自定义函数组件
- BizLoggerFunction: 标记自定义函数, 配合SpEL
1.1.1 EnableBizLogger
添加在配置类或启动类上自动完成框架的装配工作
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(BizLoggerConfigureSelector.class)
@EnableSpringUtil
public @interface EnableBizLogger {
@MethodDesc("租户信息, 应用或业务隔离")
String tenant();
AdviceMode mode() default AdviceMode.PROXY;
@MethodDesc("创建日志对象执行方式")
BizLoggerExecutorType executorType() default BizLoggerExecutorType.SERIAL;
@MethodDesc("创建日志对象执行线程池Bean名称")
String executorName() default "defaultBizLoggerExecutor";
@MethodDesc("使用@BizLoggerComponent/@BizLoggerFunction 自定义函数注册时, 重复函数名称是否覆盖")
boolean overrideFunction() default false;
enum BizLoggerExecutorType {
SERIAL,
PARALLEL
}
}
1.1.2 BizLogger
利用SpEL表达式, 可配置业务的分类, 日志的可见度, 业务成功日志, 失败日志, 是否记录日志等等
java
/**
* @author hp
*/
@Meta
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(value = BizLoggers.class)
public @interface BizLogger {
@Language("SpEL")
@MethodDesc("业务序列号,用于追踪数据操作, 如: 订单编号或ID")
String bizNo();
@Language("SpEL")
@MethodDesc("业务类型, 如: 订单")
String type();
@Language("SpEL")
@MethodDesc("业务子类型, 如: 订单 toC, toB端的区分")
String subType() default "";
@Language("SpEL")
@MethodDesc("日志范围, 如: 客户可见, 业务人员可见等区分")
String scope() default "";
@Language("SpEL")
@MethodDesc("业务成功日志模版, 业务正常执行无异常抛出时为成功, 此时上下文呢中可以获取BizLoggerProperties.returnValueKey对应的方法返回值用于构造成功日志")
String successLog();
@Language("SpEL")
@MethodDesc("业务失败日志模版, 业务抛出异常时判断为失败: 此时上下文中可以获取BizLoggerProperties.throwableKey对应的异常信息用于构造异常日志")
String errorLog() default "";
@Language("SpEL")
@MethodDesc("额外信息, 如: 方法请求参数/响应参数等")
String extra() default "";
@Language("SpEL")
@MethodDesc("日志同步条件, 只接受boolean值. 如: true记录日志, false不记录日志")
String condition() default "";
@MethodDesc("在业务方法执行前/后执行日志记录, 之前=true, 之后=false, 之前场景下, 无法获取业务方法内设置的上下文变量")
boolean preInvocation() default false;
@MethodDesc("处理/同步顺序")
int order() default -1;
}
1.1.3 BizLoggerComponent
通过@Component隐式注册SpringBean, 并在SpringBoot启动时, 根据通知类的接口初始化获取标记类
java
/**
* @author hp
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface BizLoggerComponent {
@AliasFor(annotation = Component.class)
String value();
}
1.1.4 BizLoggerFunction
SpEL支持在解析上下文中注册自定义变量, 自定义函数, 并在调用时使用#variable
, #customFunction(#param1,#param2)
的格式调用变量或函数.
java
/**
* @author hp
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BizLoggerFunction {
String value();
}
1.2 自定义函数
SpEL支持在解析上下文(EvaluationContext)中注册自定义变量setVariable(String name, Object varible)
, 注册自定义函数registerFunction(String name, Method method)
, 并在调用时使用#variable
, #customFunction(#param1,#param2)
的格式调用变量或函数, 但不支持调用非public static
的函数.
如: [#{#order.purchaseName}]下了一个订单. 订单名称: [#{#ORDER_NAME(#order.orderId)}], 购买商品: [#{#order.productName}], 下单结果: [#{#_return}]
其中#ORDER_NAME
就是提前注册的自定义函数名称.
值得一提的是
-
mzt-biz-log 通过定义
接口
的方式, 实现自定义函数, 但受制于接口设计, 自定义函数只能接受一个入参. 同时因为受限于SpEL无法调用非public static
的Function, 导致此框架需要先通过正则自行解析
表达式中所有的自定义函数和入参并自行调用, 再把结果字符串替换掉原始的表达式. 导致设计者不得不增加自定义的表达式格式{``{expression}}
, 仅仅为了方便正则解析. 强行引入额外的非SpEL的学习成本, 最终效果就是代码又绕又不清晰, 而且还得维护一堆调用的引用. 使用时又得额外注意其他东西.这样设计, 好处是在非Spring环境也可以使用, 但如果使用环境已经是Spring了, 这样的设计就显得臃肿了.
-
log-record 通过注解+注册SpEL自定义Function的方式, 本文也采用此设计, 但受限于SpEL框架只能注册自定义的
public static
的方法. 调用者只能配置注解public static
方法, 如果配置了非静态方法时, 在注册Function到SpEL上下文时并不会抛出任何异常, 反而是在SpEL表达式对象调用这个自定义函数时抛出异常, 并告知不能调用非静态方法. 对于使用者来说需要提前清楚的东西稍微有点多. 而此框架并未处理此类情况.此框架的实现完全是为了在SpringBoot环境中使用, 这也是我的场景, 所以不需要像原项目那样考虑很多东西.
其次
虽然调用非静态方法的方式也有, 比如将方法所在类的对象设置为SpEL上下文中的变量, 再通过#variable.method(#param1, #param2)
的方式调用; 亦或者将方法类注册SpringBean后, 通过@beanName.method(#param1, #param2)
调用;
静态方法也可以直接通过使用T(全类名).method(#param1, #param2)
调用;
上述几种方式虽然表达式不同, 但显得mzt-biz-log
和log-record
的自定义函数设计不够完善, 当然, 这都是取舍的结果...
所以本次实现时, 结合两个框架的优点, 实现调用者使用方式统一的情况下, 支持自定义非public static
函数的调用.
通过@BizLoggerComponent
的隐式注册SpringBean(log-record的做法), 并在注册自定义函数时首先区分是否静态, 避免SpEL表达式上下文中存在非静态自定义函数从而导致调用异常, 其次, 在把字符串表达式提交给SpEL解析器解析之前, 通过正则提取自定义函数表达式#function()
的函数名称(mzt-biz-log的做法), 并在框架的自定义函数注册器中检查是否为非静态函数, 如果是, 则将字符串表达式中的#function
替换为 @beanName.method
. 完成替换后提交解析. 此时自定义函数的参数列表大小不受限制, 使用者无需关心函数是否静态, 并且在表达式的编写上也可以直接使用 #function()
, 不引入新的规范的同时, 减少了使用者需要了解的规范.
1.2.1 IBizLoggerFunctionRegistrar
在服务启动后扫描并加载自定义函数
java
/**
* @author hp
*/
public interface IBizLoggerFunctionRegistrar extends ApplicationContextAware {
void registerFunctions(EvaluationContext evaluationContext);
}
直接继承加载能力, 方法主要工作就是将自定义函数名称提取, 并判断是否为非静态函数, 并替换为bean引用的格式
java
/**
* @author hp
*/
public interface IBizLoggerFunctionResolver extends IBizLoggerFunctionRegistrar {
String resolveFunctions(String expression);
}
1.2.2 AbstractBizLoggerFunctionResolver
上述接口的基础的实现, 并根据@EnableBizLogger.overrideFunction
配置, 扫描时根据Bean顺序处理函数覆盖逻辑.
java
/**
* @author hp
*/
@Slf4j
public abstract class AbstractBizLoggerFunctionResolver implements IBizLoggerFunctionResolver {
private final static Map<String, MethodWrapper> BIZ_LOG_FUNCTION_HOLDER = Maps.newHashMap();
private final static Pattern functionLocator = Pattern.compile("#\\{\\s*(#\\w*)\\(\\s*(.*?)\\)}");
protected final String pathJoin = StrUtil.DOT;
private final String spELBeanReferencePrefix = StrUtil.AT;
protected final boolean overrideFunction;
public AbstractBizLoggerFunctionResolver(boolean overrideFunction) {
this.overrideFunction = overrideFunction;
}
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
final Map<String, Object> components = applicationContext.getBeansWithAnnotation(BizLoggerComponent.class);
if (MapUtil.isEmpty(components)) {
log.warn("No custom biz log functions found, consider annotating classes with the @BizLoggerComponent.");
}
scanFunctions(components);
}
@Override
public void registerFunctions(EvaluationContext evaluationContext) {
if (evaluationContext instanceof StandardEvaluationContext sec) {
BIZ_LOG_FUNCTION_HOLDER.forEach((name, methodWrapper) -> {
if (methodWrapper.isStatic()) {
sec.registerFunction(name, methodWrapper.method());
}
});
return;
}
log.warn("The {} provided is not a StandardEvaluationContext, Can not register functions on it.", evaluationContext.getClass());
}
@Override
public String resolveFunctions(String expression) {
final Matcher matcher = functionLocator.matcher(expression);
while (matcher.find()) {
String functionName = matcher.group(1);
final String actualName = functionName.substring(1);
if (BIZ_LOG_FUNCTION_HOLDER.containsKey(actualName)) {
final MethodWrapper methodWrapper = BIZ_LOG_FUNCTION_HOLDER.get(actualName);
if (methodWrapper.isStatic()) {
continue;
}
final String methodReference = spELBeanReferencePrefix + methodWrapper.beanName + pathJoin + methodWrapper.method().getName();
expression = expression.replace(functionName, methodReference);
}
}
return expression;
}
protected void scanFunctions(Map<String, Object> components) {
components.forEach((name, component) -> {
final Method[] methods = component.getClass().getMethods();
if (ArrayUtil.isEmpty(methods)) {
return;
}
final BizLoggerComponent bizLoggerComponent = AnnotatedElementUtils.getMergedAnnotation(component.getClass(), BizLoggerComponent.class);
assert bizLoggerComponent != null;
final String beanName = bizLoggerComponent.value();
Arrays.stream(methods)
.forEach(method -> {
final BizLoggerFunction bizLogFunction = AnnotationUtils.findAnnotation(method, BizLoggerFunction.class);
if (Objects.isNull(bizLogFunction)) {
return;
}
putFunction(beanName, bizLogFunction.value(), method);
});
});
}
protected void putFunction(String beanName, String methodName, Method method) {
Preconditions.checkArgument(StrUtil.isNotEmpty(methodName), "Function name cant be empty.");
Preconditions.checkArgument(Objects.nonNull(method), "Function cant be empty.");
final MethodWrapper methodWrapper = new MethodWrapper(method, ModifierUtil.isStatic(method), beanName);
if (overrideFunction) {
BIZ_LOG_FUNCTION_HOLDER.put(methodName, methodWrapper);
} else {
BIZ_LOG_FUNCTION_HOLDER.putIfAbsent(methodName, methodWrapper);
}
}
private record MethodWrapper(Method method, boolean isStatic, String beanName) {
public MethodWrapper {
Preconditions.checkArgument(Objects.nonNull(method), "The custom function method cant be null.");
Preconditions.checkArgument(StrUtil.isNotEmpty(beanName), "The custom function class name or bean name cant be null.");
}
}
}
1.3 日志上下文
transmittable-thread-local 是阿里针对InheritableThreadLocal
无法应对池化场景的增强设计. 具体细节这里不展开了, 直接看仓库能学到很多.
利用ThreadLocal, 构建针对全局, 线程, 方法层面的上下文变量, 使得自定义变量可以被解耦的日志切面流程获取到, 进而用于解析SpEL和构建日志数据.
结合上述两个项目对这个上下文的使用, 也是有点乱. 本文构建了基于全局 metaContextHolder
, 基于主线程 threadBasedVariableHolder
, 基于线程方法的 methodBasedVariableHolder
的三个容器.
- mzt-biz-log 采用
InheritableThreadLocal
保存变量, 无法应对池化场景 - log-record 则是使用
TransmittableThreadLocal
但是直接保存StandardEvaluationContext
, 无法应对开辟子线程中上下文变量污染问题.
本文则结合上述两者的优点进行实现.
1.3.1 BizLoggerContext
- 全局容器用于记录元数据, 比如框架装配时拿到的租户信息
- 线程级别用于记录整个链路都可以使用的变量
- 方法级别则针对开辟子线程时, 避免方法内设置的变量互相污染的问题.
java
/**
* @author hp
*/
@Slf4j
public class BizLoggerContext {
private static final Map<String, Object> metaContextHolder = new ConcurrentHashMap<>(64);
// 应对线程池化的场景, 方法级别
private static final TransmittableThreadLocal<Deque<Map<String, Object>>> methodBasedVariableHolder = new TransmittableThreadLocal<>();
// 应对线程池化的场景, 线程级别
private static final TransmittableThreadLocal<Map<String, Object>> threadBasedVariableHolder = new TransmittableThreadLocal<>();
private static final String TENANT_KEY = "_tenant";
public static void addEmptyFrame() {
Optional.ofNullable(methodBasedVariableHolder.get())
.ifPresentOrElse(
deque -> deque.push(Maps.newHashMap()),
() -> {
Deque<Map<String, Object>> deque = Queues.newArrayDeque();
deque.push(Maps.newHashMap());
methodBasedVariableHolder.set(deque);
});
}
public static void putVariable(String key, Object value) {
Optional.ofNullable(methodBasedVariableHolder.get())
.ifPresentOrElse(
deque -> {
if (deque.isEmpty()) {
deque.push(Maps.newHashMap());
}
deque.element().put(key, value);
},
() -> {
Deque<Map<String, Object>> deque = Queues.newArrayDeque();
deque.push(Maps.newHashMap());
methodBasedVariableHolder.set(deque);
deque.element().put(key, value);
});
}
public static void putGlobalVariable(String key, Object value) {
Optional.ofNullable(threadBasedVariableHolder.get())
.ifPresentOrElse(
map -> map.put(key, value),
() -> {
final Map<String, Object> map = Maps.newConcurrentMap();
threadBasedVariableHolder.set(map);
map.put(key, value);
});
}
public static boolean notEmpty() {
return MapUtil.isNotEmpty(metaContextHolder) ||
MapUtil.isNotEmpty(threadBasedVariableHolder.get()) ||
(Objects.nonNull(methodBasedVariableHolder.get()) && MapUtil.isNotEmpty(methodBasedVariableHolder.get().peek()));
}
public static Map<String, Object> getAllVariables() {
final Map<String, Object> variables = Maps.newHashMap(getGlobalVariables());
variables.putAll(getVariables());
variables.putAll(metaContextHolder);
return variables;
}
public static Map<String, Object> getVariables() {
return Optional.ofNullable(methodBasedVariableHolder.get()).map(Deque::peek).orElse(Maps.newHashMap());
}
public static Object getVariable(String name) {
return getVariables().getOrDefault(name, null);
}
public static Map<String, Object> getGlobalVariables() {
return Optional.ofNullable(threadBasedVariableHolder.get()).map(MapUtil::unmodifiable).orElse(Maps.newHashMap());
}
public static Object getGlobalVariable(String name) {
return getGlobalVariables().getOrDefault(name, null);
}
public static Object getVariableOrGlobal(String name) {
return Optional.ofNullable(getVariable(name))
.orElse(getGlobalVariable(name));
}
public static void clear() {
Optional.ofNullable(methodBasedVariableHolder.get()).ifPresent(Deque::pop);
}
public static void clearGlobal() {
Optional.ofNullable(threadBasedVariableHolder.get()).ifPresent(Map::clear);
}
public static void putTenant(String tenant) {
Preconditions.checkArgument(StrUtil.isNotEmpty(tenant), "Biz logger tenant cant be empty.");
metaContextHolder.put(TENANT_KEY, tenant);
log.info("Biz logger tenant has been set as [{}].", tenant);
}
public static String getTenant() {
return (String) metaContextHolder.getOrDefault(TENANT_KEY, null);
}
public static void putReturnValue(Object object) {
final String returnValueKey = SpringUtil.getBean(BizLoggerProperties.class).getReturnValueKey();
putVariable(returnValueKey, object);
log.debug("The method invocation return value has been set as {}", object);
}
public static Object getReturnValue() {
final String returnValueKey = SpringUtil.getBean(BizLoggerProperties.class).getReturnValueKey();
return getVariable(returnValueKey);
}
public static void putThrowable(Throwable throwable) {
final String throwableKey = SpringUtil.getBean(BizLoggerProperties.class).getThrowableKey();
putVariable(throwableKey, throwable);
log.debug("The method invocation has thrown an exception of {}.", throwable.getMessage());
}
public static Throwable getThrowable() {
final String throwableKey = SpringUtil.getBean(BizLoggerProperties.class).getThrowableKey();
return (Throwable) getVariable(throwableKey);
}
public static void putTimeCost(Long milliseconds) {
final String timeCostKey = SpringUtil.getBean(BizLoggerProperties.class).getTimeCostKey();
putVariable(timeCostKey, milliseconds);
log.debug("The method invocation costs {}ms.", milliseconds);
}
public static Long getTimeCost() {
final String timeCostKey = SpringUtil.getBean(BizLoggerProperties.class).getTimeCostKey();
return (Long) getVariable(timeCostKey);
}
}
1.4 SpEL
不同于Join框架的特点, @BizLogger
需要解析方法的入参和对应的参数名称. 此时, 仅仅使用StandardEvaluationContext
已经不能满足处理需求, 虽然JDK17等后续版本对反射的做了更严格限制, 但是SpEL还是提供了MethodBasedEvaluationContext
, 其中包含了ParameterNameDiscoverer
用于解析入参和入参的名称.
可以从MethodBasedEvaluationContext
中看出默认将方法入参以a
或p
开头, 拼接参数列表索引, 如a0
,p1
的方式, 将方法入参设置为SpEL上下文的自定义变量, 方便获取. 实测SpringBoot3.2.1+JDK21使用此方式, 还是可以将参数名称正确解析.
- mzt-biz-log 采用上述方式实现
- log-record 则直接使用
StandardEvaluationContext
, 导致其必须自主解析方法入参, 并仅使用p
+参数列表索引的方式引用方法入参.
本文则实现MethodBasedEvaluationContext
把规范解析入参这部分工作交给SpEL框架.
1.4.1 BizLoggerEvaluationContext
在构造本次方法的上下文时, 将日志上下文中已有的各级变量按顺序设置上.
java
/**
* @author hp
*/
public class BizLoggerEvaluationContext extends MethodBasedEvaluationContext {
public BizLoggerEvaluationContext(
Object rootObject,
Method method,
Object[] arguments,
ParameterNameDiscoverer parameterNameDiscoverer
) {
super(rootObject, method, arguments, parameterNameDiscoverer);
if (BizLoggerContext.notEmpty()) {
BizLoggerContext.getAllVariables().forEach(this::setVariable);
}
}
}
1.4.2 BizLoggerExpressionEvaluator
相比于常用的SpelExpressionParser
, CachedExpressionEvaluator
主要是对表达式解析做了缓存处理.
- mzt-biz-log: 虽然采用
CachedExpressionEvaluator
增加了缓存操作, 但是因为其引入了自定义格式{``{}}
, 导致解析繁琐, 多了自主正则提取的再用SpEL解析, 再将解析结果替换原内容的操作. - log-record: 则更加直接, 因为框架测试类里也仅配置
public static
函数, SpEL也仅支持调用自定义的public static
函数, 所以直接用SpelExpressionParser
解析并获取解析结果.
本文则针对CachedExpressionEvaluator
的实现重载其中的解析方法, 增加模版上下文能力ParserContext
(SpEL标准组件), 提供默认模版, 这就是SpEL文档最后提到的模版解析expressions-templating.
支持仅解析#{}
内部的内容, 模版以外的内容将作为字符串保留.
如: [#{#order.purchaseName}]下了一个订单. 订单名称: [#{#ORDER_NAME(#order.orderId)}], 购买商品: [#{#order.productName}], 下单结果: [#{#_return}].
java
/**
* @author hp
*/
@Slf4j
public class BizLoggerExpressionEvaluator extends CachedExpressionEvaluator {
private final Map<ExpressionKey, Expression> expressionCache = Maps.newConcurrentMap();
private final ParserContext parserContext = ParserContext.TEMPLATE_EXPRESSION;
private final ExpressionParser expressionParser;
private final IBizLoggerFunctionResolver bizLoggerFunctionResolver;
public BizLoggerExpressionEvaluator(SpelExpressionParser expressionParser) {
super(expressionParser);
this.expressionParser = expressionParser;
this.bizLoggerFunctionResolver = SpringUtil.getBean(IBizLoggerFunctionResolver.class);
}
public <T> T parseExpression(String expression, Class<?> targetClass, Method method, EvaluationContext evaluationContext) {
if (StrUtil.isEmpty(expression)) {
return null;
}
return parseExpression(expression, new AnnotatedElementKey(method, targetClass), evaluationContext);
}
@SuppressWarnings("unchecked")
protected <T> T parseExpression(String expression, AnnotatedElementKey methodKey, EvaluationContext evaluationContext) {
final String resolvedExpression = bizLoggerFunctionResolver.resolveFunctions(expression);
log.debug("The expression being resolved is {}", resolvedExpression);
return (T) getExpression(this.expressionCache, methodKey, resolvedExpression).getValue(evaluationContext);
}
@Nonnull
@Override
protected Expression parseExpression(@Nonnull String expression) {
return expressionParser.parseExpression(expression, parserContext);
}
}
1.5 解析注解并生成日志对象
1.5.1 BizLogDTO
上述两个框架这里的设计都大同小异.
- mzt-biz-log: 通过自定义函数能力, 提供了一个
_Diff
函数用于记录属性值变化 - log-record: 基本和上述相同. 只是在调用函数, 和对函数结果的处理上不一样而已.
本文V1版本仅实现了操作日志, DiffLog功能将在V2中实现. 因为上述两个的_Diff
也都挺乱的.
java
/**
* @author hp
*/
@Data
@Builder
public class BizLogDTO {
private String tenant;
private String bizNo;
private String type;
private String subType;
private String scope;
private String action;
private BizLogOperator operator;
private Instant operatedAt;
private String extra;
private boolean condition;
private boolean succeed;
private Long timeCost;
// private List<DiffLog> diffs;
public void succeed(String action) {
this.succeed = true;
this.action = action;
}
public void failed(String action) {
this.succeed = false;
this.action = action;
}
}
1.5.1 IBizLogCreatorsExecutorFactory
IBizLogCreatorsExecutorFactory
在本文是通过注解中的表达式, 构建日志对象的唯一入口. 本节主要是说明这个流程.
- mzt-biz-log: 源码中则是零散的对表达式提取, 再根据注解的映射关系, 对应解析结果, 不仅代码杂乱, 方法的定义上也像是直接通过IDE抽取自动生成的一样.
- log-record: 则是直接事务脚本式的解析并赋值.
上述两者写的切面类都给人一种杂乱, 臃肿的感觉, 并且哪些值要放在日志上下文中传递, 哪些不需要这样做也是很随心所欲. 并且这种通过解析表达式, 同时表达式解析取值时还可能存在额外的方法调用的情况下, 两个框架都是用方法线程串行调用, 可能会比较耗时.
其实在如何解析一个由SpEL表达式构成的注解, 并将解析数据封装为对象的逻辑上, Join框架的factory包已经提供了一个设计模版.
简单来说就是, 工厂负责构建具体如何解析, 并通过解析模版创建解析类, 再根据串并行配置, 编排顺序后统一提交执行解析.
java
/**
* @author hp
*/
public interface IBizLogCreatorsExecutorFactory {
IBizLogCreatorsExecutor createFor(Class<?> targetClass, Method method);
}
/**
* @author hp
*/
@Slf4j
public class DefaultBizLogCreatorsExecutorFactory implements IBizLogCreatorsExecutorFactory {
private final Map<AnnotatedElementKey, IBizLogCreatorsExecutor> cache = Maps.newConcurrentMap();
private final AnnotationAttributes enableBizLog;
private final List<IBizLogCreatorFactory> bizLogCreatorFactories;
private final Map<String, ExecutorService> executorServiceMap;
private final BizLoggerExceptionNotifier bizLoggerExceptionNotifier;
private final BeanResolver beanResolver;
public DefaultBizLogCreatorsExecutorFactory(
AnnotationAttributes enableBizLog,
List<IBizLogCreatorFactory> bizLogCreatorFactories,
Map<String, ExecutorService> executorServiceMap,
BizLoggerExceptionNotifier bizLoggerExceptionNotifier,
BeanResolver beanResolver
) {
this.enableBizLog = enableBizLog;
this.bizLogCreatorFactories = bizLogCreatorFactories;
AnnotationAwareOrderComparator.sort(this.bizLogCreatorFactories);
this.executorServiceMap = executorServiceMap;
this.bizLoggerExceptionNotifier = bizLoggerExceptionNotifier;
this.beanResolver = beanResolver;
}
@Override
public IBizLogCreatorsExecutor createFor(Class<?> targetClass, Method method) {
final AnnotatedElementKey key = new AnnotatedElementKey(method, targetClass);
return this.cache.computeIfAbsent(key, k -> createCreatorsExecutor(targetClass, method));
}
private IBizLogCreatorsExecutor createCreatorsExecutor(Class<?> targetClass, Method method) {
final List<IBizLogCreator> creators = this.bizLogCreatorFactories.stream()
.flatMap(factory -> factory.createFor(targetClass, method).stream())
.toList();
return buildCreatorsExecutor(targetClass, method, enableBizLog, creators);
}
private IBizLogCreatorsExecutor buildCreatorsExecutor(Class<?> targetClass, Method method, AnnotationAttributes enableBizLog, List<IBizLogCreator> creators) {
if (enableBizLog == null || enableBizLog.getEnum("executorType") == EnableBizLogger.BizLoggerExecutorType.SERIAL) {
log.debug("Biz Logger for {}.{} uses serial executor", targetClass, method);
return new SerialBizLogCreatorExecutor(beanResolver, creators);
}
final String executorName = enableBizLog.getString("executorName");
if (enableBizLog.getEnum("executorType") == EnableBizLogger.BizLoggerExecutorType.PARALLEL) {
log.debug("Biz Logger for {}.{} uses parallel executor, the executor pool is {}", targetClass, method, executorName);
final ExecutorService executorService = executorServiceMap.get(executorName);
Preconditions.checkArgument(executorService != null, "The required executorService=%s doesn't exist.".formatted(executorName));
return new ParallelBizLogCreatorExecutor(
beanResolver,
creators,
executorService,
bizLoggerExceptionNotifier
);
}
throw new IllegalArgumentException("无效类型");
}
}
1.5.2 IBizLogCreatorsExecutor
执行解析器的执行器, 主要作用就是区分串并行任务. 例如如下的 并行执行器.
java
/**
* @author hp
*/
public interface IBizLogCreatorsExecutor {
Collection<BizLogDTO> execute(MethodInvocationWrapper invocationWrapper, boolean preInvocation);
}
/**
* @author hp
*/
@Slf4j
public class ParallelBizLogCreatorExecutor extends AbstractBizLogCreatorsExecutor {
private final ExecutorService executorService;
private final BizLoggerExceptionNotifier exceptionNotifier;
private final List<ExecutorWithLevel> executorWithLevels;
public ParallelBizLogCreatorExecutor(
BeanResolver beanResolver,
List<IBizLogCreator> creators,
ExecutorService executorService,
BizLoggerExceptionNotifier exceptionNotifier
) {
super(beanResolver, creators);
this.executorService = executorService;
this.exceptionNotifier = exceptionNotifier;
this.executorWithLevels = buildExecutorWithLevel();
}
private List<ExecutorWithLevel> buildExecutorWithLevel() {
return getCreators()
.stream()
.collect(Collectors.groupingBy(IBizLogCreator::runOnLevel))
.entrySet()
.stream()
.map(entry -> new ExecutorWithLevel(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(ExecutorWithLevel::level))
.collect(Collectors.toList());
}
@Override
public Collection<BizLogDTO> execute(MethodInvocationWrapper invocationWrapper, boolean preInvocation) {
final EvaluationContext evaluationContext = BizLoggerEvaluationContextFactory.createEvaluationContext(
invocationWrapper.getMethod(),
invocationWrapper.getArgs(),
invocationWrapper.getTargetClass(),
getBeanResolver()
);
return executeCreation(evaluationContext, preInvocation);
}
private List<BizLogDTO> executeCreation(EvaluationContext evaluationContext, boolean preInvocation) {
return this.executorWithLevels.stream()
.flatMap(leveledTasks -> {
log.debug("Run creation on level {} use {}", leveledTasks.level(), leveledTasks.creators());
final List<Task> tasks = buildCreationTasks(leveledTasks, evaluationContext, preInvocation);
if (CollUtil.isEmpty(tasks)) {
return Stream.empty();
}
try {
if (log.isDebugEnabled()) {
StopWatch stopwatch = new StopWatch("Starting executing creation tasks");
stopwatch.start();
final Stream<BizLogDTO> bizLogDTOs = this.executorService.invokeAll(tasks)
.stream()
.map(f -> {
try {
return f.get();
} catch (InterruptedException | ExecutionException e) {
exceptionNotifier.handle().accept(evaluationContext, e);
return null;
}
})
.filter(Objects::nonNull);
stopwatch.stop();
log.debug("Run execute cost {} ms, task is {}.", stopwatch.getTotalTimeMillis(), tasks);
return bizLogDTOs;
} else {
return this.executorService.invokeAll(tasks)
.stream()
.map(f -> {
try {
return f.get();
} catch (InterruptedException | ExecutionException e) {
exceptionNotifier.handle().accept(evaluationContext, e);
return null;
}
})
.filter(Objects::nonNull);
}
} catch (InterruptedException e) {
throw new BizLoggerException(BizLoggerErrorCode.async_creation_error, e);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private List<Task> buildCreationTasks(ExecutorWithLevel leveledExecutors, EvaluationContext evaluationContext, boolean preInvocation) {
return leveledExecutors.creators()
.stream()
.filter(c -> c.preInvocation() == preInvocation)
.map(executor -> new Task(executor::createLog, evaluationContext, exceptionNotifier))
.collect(Collectors.toList());
}
@AllArgsConstructor
static class Task implements Callable<BizLogDTO> {
private final Function<EvaluationContext, BizLogDTO> function;
private final EvaluationContext evaluationContext;
private final BizLoggerExceptionNotifier exceptionNotifier;
@Override
public BizLogDTO call() {
try {
return function.apply(evaluationContext);
} catch (Exception e) {
exceptionNotifier.handle().accept(evaluationContext, e);
}
return null;
}
}
record ExecutorWithLevel(Integer level, List<IBizLogCreator> creators) {
}
}
1.5.3 IBizLogCreatorFactory
通过工厂创建日志对象的处理器, 处理器的方法, 则由工厂定义, 处理器只负责编排和调用, 不关心方法具体逻辑.
java
/**
* @author hp
*/
public interface IBizLogCreatorFactory {
List<IBizLogCreator> createFor(Class<?> targetClass, Method method);
}
/**
* @author hp
*/
public abstract class AbstractAnnotationBasedBizLogCreatorFactory<A extends Annotation> implements IBizLogCreatorFactory {
protected final Class<A> annotationClass;
public AbstractAnnotationBasedBizLogCreatorFactory(Class<A> annotationClass) {
this.annotationClass = annotationClass;
}
@Override
public List<IBizLogCreator> createFor(Class<?> targetClass, Method method) {
return createBizLoggerExecutors(targetClass, method);
}
protected List<IBizLogCreator> createBizLoggerExecutors(Class<?> targetClass, Method method) {
if (!AnnotatedElementUtils.isAnnotated(method, annotationClass)) {
return Collections.emptyList();
}
final Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, annotationClass);
return annotations.stream()
.map(annotation -> createBizLoggerExecutor(targetClass, method, annotation))
.toList();
}
protected IBizLogCreator createBizLoggerExecutor(Class<?> targetClass, Method method, A annotation) {
return new DefaultBizLogCreator(
createForOrder(targetClass, method, annotation),
createForPreInvocation(targetClass, method, annotation),
createForBizNo(targetClass, method, annotation),
createForType(targetClass, method, annotation),
createForSubType(targetClass, method, annotation),
createForScope(targetClass, method, annotation),
createForOperator(targetClass, method, annotation),
createForSuccessLog(targetClass, method, annotation),
createForErrorLog(targetClass, method, annotation),
createForExtra(targetClass, method, annotation),
createForCondition(targetClass, method, annotation)
);
}
protected abstract int createForOrder(Class<?> targetClass, Method method, A annotation);
protected abstract boolean createForPreInvocation(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForBizNo(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForType(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForSubType(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForScope(Class<?> targetClass, Method method, A annotation);
protected abstract BizLogOperator createForOperator(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForSuccessLog(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForErrorLog(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, String> createForExtra(Class<?> targetClass, Method method, A annotation);
protected abstract Function<EvaluationContext, Boolean> createForCondition(Class<?> targetClass, Method method, A annotation);
}
解析@BizLogger
注解的工厂实现
java
/**
* @author hp
*/
@Slf4j
public class BizLoggerBasedBizLogCreatorFactory extends AbstractAnnotationBasedBizLogCreatorFactory<BizLogger> {
private final BizLoggerExpressionEvaluator bizLoggerExpressionEvaluator = new BizLoggerExpressionEvaluator(new SpelExpressionParser());
private final IBizLogOperatorService bizLogOperatorService;
public BizLoggerBasedBizLogCreatorFactory(ObjectProvider<IBizLogOperatorService> bizLogOperatorService) {
super(BizLogger.class);
this.bizLogOperatorService = bizLogOperatorService.getIfAvailable();
}
@Override
protected int createForOrder(Class<?> targetClass, Method method, BizLogger annotation) {
return annotation.order();
}
@Override
protected boolean createForPreInvocation(Class<?> targetClass, Method method, BizLogger annotation) {
return annotation.preInvocation();
}
@Override
protected Function<EvaluationContext, String> createForBizNo(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.bizNo(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, String> createForType(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.type(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, String> createForSubType(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.subType(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, String> createForScope(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.scope(), targetClass, method, context);
}
@Override
protected BizLogOperator createForOperator(Class<?> targetClass, Method method, BizLogger annotation) {
return bizLogOperatorService.get();
}
@Override
protected Function<EvaluationContext, String> createForSuccessLog(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.successLog(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, String> createForErrorLog(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.errorLog(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, String> createForExtra(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.extra(), targetClass, method, context);
}
@Override
protected Function<EvaluationContext, Boolean> createForCondition(Class<?> targetClass, Method method, BizLogger annotation) {
return context -> bizLoggerExpressionEvaluator.parseExpression(annotation.condition(), targetClass, method, context);
}
}
1.5.4 IBizLogCreator
仅仅关心方法的编排和调用, 方法逻辑由工厂提供.
java
/**
* @author hp
*/
public interface IBizLogCreator {
BizLogDTO createLog(EvaluationContext evaluationContext);
default boolean preInvocation() {
return false;
}
default int runOnLevel() {
return 0;
}
}
/**
* @author hp
*/
public abstract class AbstractBizLogCreator implements IBizLogCreator {
protected abstract String bizNo(EvaluationContext evaluationContext);
protected abstract String type(EvaluationContext evaluationContext);
protected abstract String subType(EvaluationContext evaluationContext);
protected abstract String scope(EvaluationContext evaluationContext);
protected abstract BizLogOperator operator();
protected abstract String successLog(EvaluationContext evaluationContext);
protected abstract String errorLog(EvaluationContext evaluationContext);
protected abstract String extra(EvaluationContext evaluationContext);
protected abstract boolean condition(EvaluationContext evaluationContext);
@Override
public BizLogDTO createLog(EvaluationContext evaluationContext) {
final BizLogDTO bizLogDTO = BizLogDTO.builder()
.tenant(BizLoggerContext.getTenant())
.bizNo(bizNo(evaluationContext))
.type(type(evaluationContext))
.subType(subType(evaluationContext))
.scope(scope(evaluationContext))
.operator(operator())
.operatedAt(Instant.now())
.timeCost(BizLoggerContext.getTimeCost())
.condition(condition(evaluationContext))
.extra(extra(evaluationContext))
.build();
Optional.ofNullable(BizLoggerContext.getThrowable())
.ifPresentOrElse(
thr -> bizLogDTO.failed(errorLog(evaluationContext)),
() -> bizLogDTO.succeed(successLog(evaluationContext))
);
return bizLogDTO;
}
}
1.6 Aspect
在切面设计上
- mzt-biz-log: 为了兼容性, 自主实现切面相关组件和配置. 使用一个wrapper类处理业务的调用, 这个很不戳.
- log-record: 本身就是为了SpringBoot环境构建的, 直接使用注解配置. 类似
mzt-biz-log
, 它也增加了Stopwatch的记录, 但代码整体看着还是比较乱, 因为它全塞到一个方法里了, 方法超级长.
本文也是通过注解配置实现切面. 总体来说实现上log-record
做的更简单易懂, 但设计没mzt-biz-log
好.
1.6.1 BizLoggerAspect
同样的使用了一个wrapper类封装业务方法的调用和异常捕获.
MethodInvocationWrapper
- mzt-biz-log: 用法偏贫血模型那样
- log-record: 没这设计
本文更偏向充血的方向, 将逻辑内聚一些.
java
/**
* @author hp
*/
public class MethodInvocationWrapper {
private final ProceedingJoinPoint joinPoint;
private final StopWatch stopWatch;
@Getter
private final Class<?> targetClass;
@Getter
private final Method method;
@Getter
private final Object[] args;
private boolean success = false;
@Getter
private Object result = null;
@Getter
private Throwable throwable;
public MethodInvocationWrapper(ProceedingJoinPoint joinPoint, StopWatch stopWatch) {
Preconditions.checkArgument(Objects.nonNull(joinPoint), "The joinPoint cant be null.");
Preconditions.checkArgument(Objects.nonNull(stopWatch), "The stopWatch cant be null.");
Preconditions.checkArgument(joinPoint.getSignature() instanceof MethodSignature, "The ProceedingJoinPoint has to be a MethodSignature type.");
this.joinPoint = joinPoint;
this.targetClass = getTargetClass(joinPoint.getTarget());
this.method = ((MethodSignature) joinPoint.getSignature()).getMethod();
this.args = joinPoint.getArgs();
this.stopWatch = stopWatch;
if (stopWatch.isRunning()) {
stopWatch.stop();
}
}
public void proceed() {
stopWatch.start(IBizLoggerPerformanceMonitor.MONITOR_TASK_INVOCATION);
try {
this.result = joinPoint.proceed();
this.success = true;
BizLoggerContext.putReturnValue(this.result);
} catch (Throwable e) {
this.success = false;
this.throwable = e;
BizLoggerContext.putThrowable(this.throwable);
}
stopWatch.stop();
BizLoggerContext.putTimeCost(stopWatch.lastTaskInfo().getTimeMillis());
}
public boolean failed() {
return Objects.nonNull(this.throwable) || !this.success;
}
private static Class<?> getTargetClass(Object target) {
return AopProxyUtils.ultimateTargetClass(target);
}
public void throwException() throws Throwable {
if (Objects.isNull(this.throwable)) {
return;
}
throw this.throwable;
}
}
BizLoggerAspect
-
mzt-biz-log: 你就看吧, 一看一个不吱声;
同步日志的逻辑, 通过预留接口, 把实现交给使用方, 并通过方法线程调用.
-
log-record: 你就看吧, 一看一个不吱声;
同步日志的逻辑, 考虑了串并行方式, 直接内聚在切面中, 并在实现上支持多种管道. 提供了常用消息队列的配置(都是最基本配置, 到手大概率还得自定义)等等.
本文在构建日志对象BizLogDTO
的逻辑上, 因为引入工厂模式, 收敛了入口, 整体代码会少些. 同步方式目前也只是通过方法线程简单调用接口, 实现留给使用方, 后续可能会调整, 上述工厂设计(串并行执行器部分)可以应用到这里.
java
/**
* @author hp
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
public class BizLoggerAspect {
private final ObjectProvider<IBizLoggerPerformanceMonitor> bizLogPerformanceMonitor;
private final ObjectProvider<IBizLogCreatorsExecutorFactory> bizLogCreatorsExecutorFactory;
private final ObjectProvider<IBizLogSyncService> bizLogSyncService;
@Around("@annotation(com.luban.biz.logger.annotation.BizLoggers) || @annotation(com.luban.biz.logger.annotation.BizLogger)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
BizLoggerContext.addEmptyFrame();
final StopWatch stopWatch = new StopWatch(MONITOR_NAME);
final MethodInvocationWrapper invocationWrapper = new MethodInvocationWrapper(joinPoint, stopWatch);
final Collection<BizLogDTO> preInvocationLogs = preInvocation(invocationWrapper, stopWatch);
invocationWrapper.proceed();
final Collection<BizLogDTO> postInvocationLogs = postInvocation(invocationWrapper, stopWatch);
syncLogs(preInvocationLogs, postInvocationLogs, stopWatch);
if (invocationWrapper.failed()) {
invocationWrapper.throwException();
}
return invocationWrapper.getResult();
}
private void syncLogs(Collection<BizLogDTO> preInvocationLogs, Collection<BizLogDTO> postInvocationLogs, StopWatch stopWatch) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
final List<BizLogDTO> logs = Lists.newArrayList(preInvocationLogs);
logs.addAll(postInvocationLogs);
stopWatch.start(MONITOR_TASK_SYNC_LOGS);
try {
Optional.ofNullable(bizLogSyncService.getIfAvailable()).ifPresent(service -> service.sync(logs));
} catch (Exception e) {
log.error("BizLoggerAspect.doAround - syncLogs - Sync logs failed:", e);
} finally {
stopWatch.stop();
}
}
private Collection<BizLogDTO> preInvocation(MethodInvocationWrapper invocationWrapper, StopWatch stopWatch) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
stopWatch.start(MONITOR_TASK_BEFORE_INVOCATION);
Collection<BizLogDTO> bizLogDTOs = Collections.emptyList();
try {
bizLogDTOs = Optional.ofNullable(bizLogCreatorsExecutorFactory.getIfAvailable())
.map(factory -> factory.createFor(invocationWrapper.getTargetClass(), invocationWrapper.getMethod()))
.map(executor -> executor.execute(invocationWrapper, true))
.orElse(Collections.emptyList());
} catch (Exception e) {
log.error("BizLoggerAspect.doAround - preInvocation - Creating BizLogCreators failed:", e);
} finally {
stopWatch.stop();
}
return bizLogDTOs;
}
private Collection<BizLogDTO> postInvocation(MethodInvocationWrapper invocationWrapper, StopWatch stopWatch) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
stopWatch.start(MONITOR_TASK_AFTER_INVOCATION);
Collection<BizLogDTO> bizLogDTOs = Collections.emptyList();
try {
bizLogDTOs = Optional.ofNullable(bizLogCreatorsExecutorFactory.getIfAvailable())
.map(factory -> factory.createFor(invocationWrapper.getTargetClass(), invocationWrapper.getMethod()))
.map(executor -> executor.execute(invocationWrapper, false))
.orElse(Collections.emptyList());
} catch (Exception e) {
log.error("BizLoggerAspect.doAround - postInvocation - Creating BizLogCreators failed:", e);
} finally {
stopWatch.stop();
}
try {
Optional.ofNullable(this.bizLogPerformanceMonitor.getIfAvailable())
.ifPresent(monitor -> monitor.print(stopWatch));
} catch (Exception e) {
log.error("BizLoggerAspect.doAround - postInvocation - Monitoring biz logger performance failed:", e);
}
return bizLogDTOs;
}
}
1.7 Configuration
加载方式照抄@EnableTransactionManagement
1.7.1 BizLoggerConfigureSelector
java
/**
* @author hp
*/
public class BizLoggerConfigureSelector extends AdviceModeImportSelector<EnableBizLogger> {
@Override
protected String[] selectImports(AdviceMode adviceMode) {
return switch (adviceMode) {
case PROXY -> new String[]{AutoProxyRegistrar.class.getName(), BizLoggerAutoConfiguration.class.getName()};
case ASPECTJ -> new String[]{BizLoggerAutoConfiguration.class.getName()};
};
}
}
1.7.2 BizLoggerAutoConfiguration
- mzt-biz-log: 使用
@Enablexxx
注解设计, 基本上就是照搬@EnableTransactionManagement
- log-record: 使用Spring的imports文件, 涉及SpringBoot高低版本对imports文件格式要求不一致的问题, 所以导致它需要提供SpringBoot3x和2x的插件包.
本文采用mzt-biz-log
的设计, 相对灵活些.
java
/**
* @author hp
*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@Import(BizLoggerProperties.class)
public class BizLoggerAutoConfiguration implements ImportAware {
private AnnotationAttributes enableBizLog;
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.enableBizLog = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableBizLogger.class.getName(), false)
);
if (this.enableBizLog == null) {
log.info("Annotate the spring boot application class with the @EnableBizLog to enable the biz logger.");
}
initMetaBizLogContext();
}
private void initMetaBizLogContext() {
BizLoggerContext.putTenant(enableBizLog.getString("tenant"));
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BizLoggerAspect bizLogAspect(
ObjectProvider<IBizLoggerPerformanceMonitor> bizLogPerformanceMonitor,
ObjectProvider<IBizLogCreatorsExecutorFactory> bizLogCreatorsExecutorFactory,
ObjectProvider<IBizLogSyncService> bizLogSyncService
) {
return new BizLoggerAspect(
bizLogPerformanceMonitor,
bizLogCreatorsExecutorFactory,
bizLogSyncService
);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(IBizLogCreatorsExecutorFactory.class)
public IBizLogCreatorsExecutorFactory bizLogCreatorsExecutorFactory(
List<IBizLogCreatorFactory> bizLogCreatorFactories,
Map<String, ExecutorService> executorServiceMap,
BizLoggerExceptionNotifier bizLoggerExceptionNotifier,
BeanFactory beanFactory
) {
return new DefaultBizLogCreatorsExecutorFactory(
enableBizLog,
bizLogCreatorFactories,
executorServiceMap,
bizLoggerExceptionNotifier,
new BeanFactoryResolver(beanFactory)
);
}
@Bean
@Role(BeanDefinition.ROLE_SUPPORT)
public ExecutorService defaultBizLoggerExecutor() {
final BasicThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("BizLogger-Thread-%d")
.daemon(true)
.build();
int maxSize = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
0,
maxSize,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
basicThreadFactory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
@Bean
@ConditionalOnMissingBean(BizLoggerExceptionNotifier.class)
public BizLoggerExceptionNotifier bizLoggerExceptionNotifier() {
return () -> (data, ex) -> {
log.error("Biz Logger Exception: ", ex);
log.error("Exception context={}", data);
};
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(IBizLogCreatorFactory.class)
public IBizLogCreatorFactory bizLogCreatorFactory(
ObjectProvider<IBizLogOperatorService> bizLogOperatorService
) {
return new BizLoggerBasedBizLogCreatorFactory(bizLogOperatorService);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(IBizLoggerFunctionRegistrar.class)
public IBizLoggerFunctionResolver bizLoggerFunctionResolver() {
return new AbstractBizLoggerFunctionResolver(
enableBizLog.getBoolean("overrideFunction")
) {};
}
@Bean
@Role(BeanDefinition.ROLE_APPLICATION)
@ConditionalOnMissingBean(IBizLogOperatorService.class)
public IBizLogOperatorService bizLogOperatorService() {
return () -> new BizLogOperator("","BizLogger-Default-Operator");
}
@Bean
@Role(BeanDefinition.ROLE_SUPPORT)
@ConditionalOnMissingBean(IBizLoggerPerformanceMonitor.class)
public IBizLoggerPerformanceMonitor bizLogPerformanceMonitor() {
return stopWatch -> log.debug("BizLogger performance: {}", stopWatch.prettyPrint(TimeUnit.NANOSECONDS));
}
@Bean
@Role(BeanDefinition.ROLE_SUPPORT)
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.defaultPrettyPrinter(new DefaultPrettyPrinter())
.addModule(new JavaTimeModule())
.build();
}
@Bean
@Role(BeanDefinition.ROLE_APPLICATION)
@ConditionalOnMissingBean(IBizLogSyncService.class)
public IBizLogSyncService bizLogSyncService(ObjectMapper objectMapper) {
return logs -> {
if (CollUtil.isEmpty(logs)) {
return;
}
logs.forEach(bizLog -> {
try {
log.info("{}", objectMapper.writeValueAsString(bizLog));
} catch (JsonProcessingException e) {
log.error("Biz Logger - Default bizLogSyncService failed: ", e);
}
});
};
}
}
二、其他杂类
2.1 services
2.1.1 IBizLoggerPerformanceMonitor
- mzt-biz-log: 原本的设计
- log-record: 事务脚本
本文同mzt-biz-log
设计
java
/**
* @author hp
*/
@FunctionalInterface
public interface IBizLoggerPerformanceMonitor {
String MONITOR_NAME = "biz-logger-performance";
String MONITOR_TASK_SYNC_LOGS = "sync-logs";
String MONITOR_TASK_BEFORE_INVOCATION = "before-invocation";
String MONITOR_TASK_INVOCATION = "invocation";
String MONITOR_TASK_AFTER_INVOCATION = "after-invocation";
void print(StopWatch stopWatch);
}
2.1.2 IBizLogOperatorService
业务操作人, 一般web应用会通过上下文容器存储当前用户信息. 通过该接口获取当前业务操作人信息.
java
/**
* @author hp
*/
@FunctionalInterface
public interface IBizLogOperatorService {
BizLogOperator get();
}
2.1.3 IBizLogSyncService
同步日志操作, 具体逻辑交给使用者定义, 发消息队列也好, 直接落库也好, 直接log打印也好.
java
/**
* @author hp
*/
public interface IBizLogSyncService {
void sync(Collection<BizLogDTO> bizLogDTOs);
}
2.1.4 BizLoggerProperties
上述两个框架都提供了方法返回值
和方法抛出的异常
这两个日志上下文变量, 但直接写死.
本文则尝试将这些能被使用者在编写SpEL表达式
时用到的变量作为可配置的. 这仅仅是一种取舍或者公约而已.
java
/**
* @author hp
*/
@Data
@ConfigurationProperties(prefix = "biz.logger")
public class BizLoggerProperties {
private String returnValueKey = "_return";
private String throwableKey = "_throwable";
private String timeCostKey = "_timeCost";
}