栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

开源:如何优雅的实现一个操作日志组件

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

开源:如何优雅的实现一个操作日志组件

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

    基于SpringBoot能够快速接入

    对业务代码具有低入侵性

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 {
    
    boolean executeBefore();

    
    String functionName();

    
    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 {
    
    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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/769063.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号