写点什么

如何优雅的记录操作日志

作者:flyhero
  • 2022 年 4 月 12 日
  • 本文字数:2856 字

    阅读完需:约 9 分钟

如何优雅的记录操作日志

1. 背景

日志几乎存在于所有系统中,开发调试日志的记录我们有 log4j,logback 等来实现,但对于要展示给用户看的日志,我并没有发现一个简单通用的实现方案。所以决定为之后的开发项目提供一个通用的操作日志组件。

2. 系统日志和操作日志

所有系统都会有日志,但我们区分了 系统日志操作日志


  • 系统日志:主要用于开发者调试排查系统问题的,不要求固定格式和可读性

  • 操作日志:主要面向用户的,要求简单易懂,反映出用户所做的动作。


通过操作日志可追溯到 某人在某时干了某事情,如:



3. 需要哪些功能

3.1 诉求:

  1. 基于 SpringBoot 能够快速接入

  2. 对业务代码具有低入侵性

3.2 解决思路:

基于以上两点,我们想想如何实现。


1、spingboot 快速接入,需要我们来自定义 spring boot starter;


2、业务入侵性低,首先想到了 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 "";}
复制代码



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@AllArgsConstructorpublic 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. 源码

发布于: 刚刚阅读数: 3
用户头像

flyhero

关注

还未添加个人签名 2019.10.11 加入

还未添加个人简介

评论

发布
暂无评论
如何优雅的记录操作日志_Java_flyhero_InfoQ写作平台