✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【架构思想】🍅
✈️本篇内容: 从0到1带你实现一个业务日志组件✈️
🍱本篇收录完整代码地址:gitee.com/diqirenge/b...
楔子
前段时间小七接到一个任务,要为某些后台操作添加业务操作日志,并且需要将日志存储在数据库中。看了看当前项目中的代码,很*很暴力,直接就在对应的地方写的一条插入语句就ok了,抛开技术不谈,这样的代码还是很优雅的!
但是小七想起了以前看过的美团的一篇技术博客------《如何优雅地记录操作日志?》 ,正好有2天时间完成这个需求,时间充足,还等什么?开干
分析需求
业务方需求很简单,总结起来就是 把什么人在什么时间对什么东西做了什么操作 给记录下来,当然再做好一点的话,可以将两次操作的不同记录下来。我们这一次就完成基础需求就好了。
确认方案
首先我们来思考一下可行的几种方案:
-
直接持久化,在需要保存日志的地方写sql就好了。
-
将保存操作抽象提取为工具类,在需要保存日志的地方,调用工具类。
-
使用AOP生成操作日志。
-
使用canal监听数据库变化,记录操作日志。
针对第一点,优势在于不用动脑子;针对第二点,优势在于比第一点动了一点脑子。这两种方案小七都是不推荐的,耦合性太高,拓展性太低。
针对第三点,也是很多公司采取的方案,但是大家一般都只是,简单的拼接了一下出入参,结合美团的博客------《如何优雅地记录操作日志?》 我们可以考虑使用SpEL来做动态模版。这种方案可行且优雅。
针对第四点,只能根据数据库的更改做日志记录,局限太大,不考虑。
综上所述,我们最后选择使用AOP来生成动态的操作日志。
基础架构
方案确定了,接下来,我们需要定义一下我们的基础架构。因为是根据AOP来玩的,所以我们需要2个东西,注解和切面。
分支名称
231013-52javaee.com-DemandAnalysis
仓库地址
分支描述
根据需求分析,完成注解设计。
代码实现
注解一:业务日志注解 BizLog
根据需求分析,我们可以抽象出我们需要的日志属性
需求 | 翻译 |
---|---|
什么人 | 操作者 |
什么时间 | 操作时间(这个不需要传入,以系统计算为准) |
什么东西 | xx系统,xx模块,xx业务id |
什么操作 | 操作类型 |
以上属性按道理就可以完成我们的需求了,但是为了拓展性,我们可以再添加以下属性
拓展 | 翻译 |
---|---|
操作结果 | 1、成功模板 2、失败模版 |
记录条件 | 根据某些条件,判断是否需要记录日志。比如:证件类型为身份证的才记录 |
详情 | 拓展字段,存放其他不好定义的信息 |
具体代码如下:
java
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(BizLogs.class)
public @interface BizLog {
/**
* 操作者,必填
*/
String operator();
/**
* 成功模板,必填
*/
String success();
/**
* 系统,默认取spring-application-name
*/
String system() default "";
/**
* 模块
*/
String module() default "";
/**
* 操作类型:比如增删改查
*/
String type() default "";
/**
* 关联的业务id
*/
String bizNo() default "";
/**
* 失败模板
*/
String fail() default "";
/**
* 拓展字段
* 记录更详细的其他信息
*/
String detail() default "";
/**
* 记录条件 默认 true
* true代表要记录,false代表不记录
*/
String condition() default "";
}
注解解析:
java
// 用于指示将被注解的元素包含在生成的Java文档中
@Documented
java
// 表明这个注解可以修饰(作用)在方法上
@Target(ElementType.METHOD)
java
// 对应Java代码的加载和运行顺序
// 范围大小:java源文件<.class文件<内存字节码
@Retention(RetentionPolicy.RUNTIME)
java
// 表明这个注解可以被子类继承
@Inherited
java
// 表示这个注解可以被重复使用
@Repeatable(BizLogs.class)
注解二: BizLogs
该注解里面只有一个BizLog注解的集合
java
BizLog[] value();
方便有多种不同日志记录需求的时候,可以写成:
java
@BizLogs({
@BizLog(.....),
@BizLog(.....)
})
切面
java
@Aspect
@Component
public class BizLogAspect {
/**
* 系统日志记录器
*/
public static Logger log = LoggerFactory.getLogger(BizLogAspect.class);
/**
* 定义切点
* 切入包含BizLog和BizLogs注解的方法
*/
@Pointcut("@annotation(com.run2code.log.annotation.BizLog) || @annotation(com.run2code.log.annotation.BizLogs)")
public void pointCut() {
}
/**
* 环绕通知
*
* @param joinPoint
* @return
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
// todo 待实现
return null;
}
}
正则表达式测试类
因为我们最终是通过解析SpEL,来完成动态模版的写入的,所以需要定义我们的数据结构,哪些能被我们的组件解析,哪些不能解析,通过文章指导,我们有2种结构需要解析:
1、直接将类的属性给到Spring去解析
比如:#request.activityId
2、将方法给到Spring去解析
比如:getActivityNameById#id
通过以上分析,我们定义以下结构:
1、类属性的结构
java
{{#request.activityId}}
2、自定义方法的结构
java
{getNameById{#id}}
通过正则表达式,我们可以很好的解析以上结构
java
/**
* 这个正则表达式的含义为:
* 匹配一个包含在花括号中的字符串,其中花括号中可以包含任意数量的空白字符(包括空格、制表符、换行符等),
* 并且花括号中至少包含一个单词字符(字母、数字或下划线)。
* =================================================
* 具体来说,该正则表达式由两部分组成:
* {s*(\w*)\s*}:表示匹配一个左花括号,后面跟随零个或多个空白字符,然后是一个单词字符(字母、数字或下划线)零个或多个空白字符,最后是一个右花括号。这部分用括号括起来,以便提取匹配到的内容。
* (.*?):表示匹配任意数量的任意字符,但尽可能少地匹配。这部分用括号括起来,以便提取匹配到的内容。
* =================================================
* 因此,整个正则表达式的意思是:
* 匹配一个包含在花括号中的字符串,
* 其中花括号中可以包含任意数量的空白字符(包括空格、制表符、换行符等),
* 并且花括号中至少包含一个单词字符(字母、数字或下划线),并提取出花括号中的内容。
* =================================================
*/
private static final Pattern PATTERN = Pattern.compile("\\{\\s*(\\w*)\\s*\\{(.*?)}}");;
测试方法
java
@Test
public void test() {
// 1、参数解析
String template = "{{#request.activityId}}";
Matcher matcher = PATTERN.matcher(template);
while (matcher.find()) {
String paramName = matcher.group(2);
System.out.println("paramName:" + paramName);
}
// 2、自定义函数解析
String customFunctionTemplate = "{getNameById{#id}}";
Matcher customFunctionMatcher = PATTERN.matcher(customFunctionTemplate);
while (customFunctionMatcher.find()) {
String paramName = customFunctionMatcher.group(2);
String funcName = customFunctionMatcher.group(1);
System.out.println("paramName:" + paramName);
System.out.println("funcName:" + funcName);
}
}
测试结果:
java
paramName:#request.activityId
paramName:#id
funcName:getNameById