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

  • GitHub: https://github.com/flyhero/easy-log
  • Gitee: https://gitee.com/flyhero/easy-log