1. 背景
日志几乎存在于所有系统中,开发调试日志的记录我们有 log4j,logback 等来实现,但对于要展示给用户看的日志,我并没有发现一个简单通用的实现方案。所以决定为之后的开发项目提供一个通用的操作日志组件。
2. 系统日志和操作日志
所有系统都会有日志,但我们区分了 系统日志 和 操作日志
通过操作日志可追溯到 某人在某时干了某事情,如:
3. 需要哪些功能
3.1 诉求:
基于 SpringBoot 能够快速接入
对业务代码具有低入侵性
3.2 解决思路:
基于以上两点,我们想想如何实现。
1、spingboot 快速接入,需要我们来自定义 spring boot starter;
2、业务入侵性低,首先想到了 AOP,一般操作日志都是在增删改查的方法中,所以我们可以使用注解在这些方法上,通过 AOP 拦截这些方法。
3.3 待实现:
因此,我们需要实现以下功能:
自定义 spring boot starter
定义日志注解
AOP 拦截日志注解方法
定义日志动态内容模板
模板中又需要实现:
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 "";
}
复制代码
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. 源码
评论