从0到1带你实现一个业务日志组件(一)需求分析篇

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:【架构思想】🍅

✈️本篇内容: 从0到1带你实现一个业务日志组件✈️

🍱本篇收录完整代码地址:gitee.com/diqirenge/b...

楔子

前段时间小七接到一个任务,要为某些后台操作添加业务操作日志,并且需要将日志存储在数据库中。看了看当前项目中的代码,很*很暴力,直接就在对应的地方写的一条插入语句就ok了,抛开技术不谈,这样的代码还是很优雅的!

但是小七想起了以前看过的美团的一篇技术博客------《如何优雅地记录操作日志?》 ,正好有2天时间完成这个需求,时间充足,还等什么?开干

分析需求

业务方需求很简单,总结起来就是 把什么人在什么时间对什么东西做了什么操作 给记录下来,当然再做好一点的话,可以将两次操作的不同记录下来。我们这一次就完成基础需求就好了。

确认方案

首先我们来思考一下可行的几种方案:

  1. 直接持久化,在需要保存日志的地方写sql就好了。

  2. 将保存操作抽象提取为工具类,在需要保存日志的地方,调用工具类。

  3. 使用AOP生成操作日志。

  4. 使用canal监听数据库变化,记录操作日志。

针对第一点,优势在于不用动脑子;针对第二点,优势在于比第一点动了一点脑子。这两种方案小七都是不推荐的,耦合性太高,拓展性太低。

针对第三点,也是很多公司采取的方案,但是大家一般都只是,简单的拼接了一下出入参,结合美团的博客------《如何优雅地记录操作日志?》 我们可以考虑使用SpEL来做动态模版。这种方案可行且优雅。

针对第四点,只能根据数据库的更改做日志记录,局限太大,不考虑。

综上所述,我们最后选择使用AOP来生成动态的操作日志。

基础架构

方案确定了,接下来,我们需要定义一下我们的基础架构。因为是根据AOP来玩的,所以我们需要2个东西,注解和切面。

分支名称

231013-52javaee.com-DemandAnalysis

仓库地址

gitee.com/diqirenge/b...

分支描述

根据需求分析,完成注解设计。

代码实现

注解一:业务日志注解 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
相关推荐
落尘2984 分钟前
Spring MVC——传递参数的方式
后端
ITCharge19 分钟前
Docker 万字教程:从入门到掌握
后端·docker·容器
落尘2981 小时前
Bean 的作用域和生命周期
后端
是店小二呀1 小时前
处理Linux下磁盘空间不足问题的实用指南
后端
落尘2981 小时前
如何通过 JWT 来解决登录认证问题
后端
是店小二呀1 小时前
处理Linux下内存泄漏问题的诊断与解决方法
后端
倚栏听风雨1 小时前
IDEA 插件开发 对文件夹下的类进行 语法检查
后端
郝同学的测开笔记1 小时前
云原生探索系列(十七):Go 语言sync.Cond
后端·云原生·go
uhakadotcom1 小时前
持续写作的“农耕思维”:如何像农民一样播种,收获稳定成长与收入
后端·面试·github
Java中文社群1 小时前
国内首个「混合推理模型」Qwen3深夜开源,盘点它的N种对接方式!
java·人工智能·后端