乐趣区

关于java:如何优雅的记录操作日志

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 诉求:

  1. 基于 SpringBoot 可能疾速接入
  2. 对业务代码具备低入侵性

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
退出移动版