共计 4108 个字符,预计需要花费 11 分钟才能阅读完成。
1. 背景
日志简直存在于所有零碎中,开发调试日志的记录咱们有 log4j,logback 等来实现,但对于要展现给用户看的日志,我并没有发现一个简略通用的实现计划。所以决定为之后的开发我的项目提供一个通用的操作日志组件。
2. 系统日志和操作日志
所有零碎都会有日志,但咱们辨别了 系统日志 和 操作日志
- 系统日志:次要用于开发者调试排查零碎问题的,不要求固定格局和可读性
- 操作日志:次要面向用户的,要求简略易懂,反映出用户所做的动作。
通过操作日志可追溯到 某人在某时干了某事件,如:
租户 | 操作人 | 工夫 | 操作 | 内容 |
---|---|---|---|---|
A 租户 | 小明 | 2022/2/27 20:15:00 | 新增 | 新增了一个用户:Mr.Wang |
B 租户 | 大米 | 2022/2/28 10:35:00 | 更新 | 批改订单 [xxxxxx] 价格为 xx 元 |
C 租户 | 老王 | 2022/2/28 22:55:00 | 查问 | 查问了名为: [xx] 的所有交易 |
3. 须要哪些性能
3.1 诉求:
- 基于 SpringBoot 可能疾速接入
- 对业务代码具备低入侵性
3.2 解决思路:
基于以上两点,咱们想想如何实现。
spingboot 疾速接入,须要咱们来自定义 spring boot starter;
业务入侵性低,首先想到了 AOP,个别操作日志都是在增删改查的办法中,所以咱们能够应用注解在这些办法上,通过 AOP 拦挡这些办法。
3.3 待实现:
因而,咱们须要实现以下性能:
- 自定义 spring boot starter
- 定义日志注解
- AOP 拦挡日志注解办法
- 定义日志动静内容模板
模板中又须要实现:
- 动静模板表达式解析:用弱小的 SpEL 来解析表达式
- 自定义函数:反对指标办法前置 / 后置的自定义函数
3.4 展示
所以咱们最终冀望的大略是这样:
@EasyLog(module = "用户模块", type = "新增", | |
content = "测试 {functionName{#userDto.name}}", | |
condition = "#userDto.name =='easylog'") | |
public String test(UserDto userDto) {return "test";} |
4. 实现步骤
4.1 定义日志注解
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface EasyLog {String tenant() default ""; | |
String operator() default ""; | |
String module() default ""; | |
String type() default ""; | |
String bizNo() default ""; | |
String content(); | |
String fail() default ""; | |
String detail() default ""; | |
String condition() default "";} |
字段 | 意义 | 反对 SpEl 表达式 | 必填 |
---|---|---|---|
tenant | 租户,SAAS 零碎中辨别不同租户 | 是 | 否 |
operator | 操作者 | 是 | 否 |
module | 模块,辨别不同业务模块 | 否 | 否 |
type | 操作类型,形如:增删改查 | 否 | 否 |
bizNo | 业务编号,便于查问 | 是 | 否 |
content | 日志模板内容 | 是 | 是 |
fail | 操作失败时的模板内容 | 是 | 否 |
detail | 额定的记录信息 | 是 | 否 |
condition | 是否记录的条件 (默认:true 记录) | 是 | 否 |
4.2 自定义函数
这里的自定义函数,并不是指 SpEL 中的自定义函数,因为 SpEL 中的自定义函数必须是静态方法才能够注册到其中,因为静态方法应用中并没有咱们本人定义方法来的不便,所以这里的自定义函数仅仅指代咱们定义的一个一般办法。
public interface ICustomFunction { | |
/** | |
* 指标办法执行前 执行自定义函数 | |
* @return 是否是前置函数 | |
*/ | |
boolean executeBefore(); | |
/** | |
* 自定义函数名 | |
* @return 自定义函数名 | |
*/ | |
String functionName(); | |
/** | |
* 自定义函数 | |
* @param param 参数 | |
* @return 执行后果 | |
*/ | |
String apply(String param); | |
} |
咱们定义好自定义函数接口,实现交给使用者。使用者将实现类交给 Spring 容器治理,咱们解析的时候从 Spring 容器中获取即可。
4.3 SpEL 表达式解析
次要关涉上面几个外围类:
- 解析器 ExpressionParser,用于将字符串表达式转换为 Expression 表达式对象。
- 表达式 Expression,最初通过它的 getValute 办法对表达式进行计算取值。
- 上下文 EvaluationContext,通过上下文对象联合表达式来计算最初的后果。
ExpressionParser parser =new SpelExpressionParser(); // 创立一个表达式解析器 | |
StandardEvaluationContext ex = new StandardEvaluationContext(); // 创立上下文 | |
ex.setVariables("name", "easylog"); // 将自定义参数增加到上下文 | |
Expression exp = parser.parseExpression("'欢送你!'+ #name"); // 模板解析 | |
String val = exp.getValue(ex,String.class); // 获取值 |
咱们只须要拿到日志注解中的动静模板即可通过 SpEL 来解析。
4.4 自定义函数的解析
咱们采纳 {functionName { param}}
的模式在模板中展现自定义函数,解析整个模板前,咱们先来解析下自定义函数,将解析后的值替换掉模板中的字符串即可。
if (template.contains("{")) {Matcher matcher = PATTERN.matcher(template); | |
while (matcher.find()) {String funcName = matcher.group(1); | |
String param = matcher.group(2); | |
if (customFunctionService.executeBefore(funcName)) {String apply = customFunctionService.apply(funcName, param); | |
} | |
} | |
} |
4.5 获取操作者信息
个别咱们都是将登录者信息存入利用上下文中,所以咱们不用每次都在日志注解中指出,咱们可对立设置,定义一个获取操作者接口,由使用者实现。
public interface IOperatorService { | |
// 获取以后操作者 | |
String getOperator(); | |
// 以后租户 | |
String getTenant();} |
4.6 定义日志内容接管
咱们要将解析实现后的日志内容实体信息发送给咱们的使用者,所以咱们须要定义一个日志接管的接口,具体的实现交给使用者来实现,无论他接管到日志存储在数据库,MQ 还是哪里,让使用者来决定。
public interface ILogRecordService { | |
/** | |
* 保留 log | |
* @param easyLogInfo 日志实体 | |
*/ | |
void record(EasyLogInfo easyLogInfo); | |
} |
4.7 定义 AOP 拦挡
@Aspect | |
@Component | |
@AllArgsConstructor | |
public class EasyLogAspect {@Pointcut("@annotation(**.EasyLog)") | |
public void pointCut() {} | |
// 盘绕告诉 | |
@Around("pointCut() && @annotation(easyLog)") | |
public Object around(ProceedingJoinPoint joinPoint, EasyLog easyLog) throws Throwable { | |
// 前置自定义函数解析 | |
try {result = joinPoint.proceed(); | |
} catch (Throwable e) { } | |
//SpEL 解析 | |
// 后置自定义函数解析 | |
return result; | |
} | |
} |
4.8 自定义 spring boot starter
创立主动配置类, 将定义的一些来交给 Spring 容器治理:
@Configuration | |
@ComponentScan("**") | |
public class EasyLogAutoConfiguration { | |
@Bean | |
@ConditionalOnMissingBean(ICustomFunction.class) | |
@Role(BeanDefinition.ROLE_APPLICATION) | |
public ICustomFunction customFunction(){return new DefaultCustomFunction(); | |
} | |
@Bean | |
@ConditionalOnMissingBean(IOperatorService.class) | |
@Role(BeanDefinition.ROLE_APPLICATION) | |
public IOperatorService operatorGetService() {return new DefaultOperatorServiceImpl(); | |
} | |
@Bean | |
@ConditionalOnMissingBean(ILogRecordService.class) | |
@Role(BeanDefinition.ROLE_APPLICATION) | |
public ILogRecordService recordService() {return new DefaultLogRecordServiceImpl(); | |
} | |
} |
上一篇我曾经残缺的介绍了如何自定义 spring boot starter,可去参考:
如何自定义 spring boot starter ?
5. 咱们能够学到什么?
你能够拉取 easy-log 源码,用于学习,通过 easy-log 你能够学到:
- 注解的定义及应用
- AOP 的利用
- SpEL 表达式的解析
- 自定义 Spring boot starter
- 设计模式
6. 源码
- GitHub: https://github.com/flyhero/easy-log
- Gitee: https://gitee.com/flyhero/easy-log