EventListener+自定义注解 实现日志的监听和异步入库

EventListener+自定义注解 实现日志的监听和异步入库

开发工作中,记录系统日志绝对是必不可少的一环。一般来说,本系统日志记录实现起来非常简单,比如在相关业务代码中执行日志insert逻辑,这种实现方式虽然简单,但却为后期维护造成一定困难也不利于阅读。而且现在动辄微服务多模块开发,耦合性太强势必造成不便,假如对接其他日志平台更是如此,所以,这里提出一种更优雅的日志记录方式,即事件监听。事件监听也是设计模式中发布-订阅模式、观察者模式的一种实现,Spring框架中内置了一个好用的观察者模式的实现,用法也很简单。

本文介绍一种基于EventListener+自定义注解实现的异步监听日志模块。
基本逻辑为:首先创建自定义日志注解@SetLog,并创建@Aspect环绕增强来处理日志对象,同时监听此日志对象 。触发后则使用@FeignClient来调用对应的日志记录逻辑执行异步入库操作。

1、简介

要想顺利的创建监听器并起作用,这个过程中需要这样几个角色:

1、事件(event)。可以封装和传递监听器中要处理的参数,如对象或字符串,并作为监听器中监听的目标。

2、监听器(listener)具体根据事件发生的业务处理模块,这里可以接收处理事件中封装的对象或字符串。

3、事件发布者(publisher)事件发生的触发者。

2、实现

本系统环境:

springBoot 2.1.5
springCloud Greenwich

下方代码在系统中的一个基础模块core-syslog中,在其他微服务中可直接在maven中引入此模块。

首先,创建日志类 SysLog,这个类将贯穿在被监听、aop、日志入库等逻辑中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
 * 日志类
 * @author lzyz.fun
 */

public class SysLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 编号
     */

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 日志类型
     */

    @NotBlank(message = "日志类型不能为空")
    private String type;
    /**
     * 日志标题
     */

    @NotBlank(message = "日志标题不能为空")
    private String title;
    /**
     * 创建者
     */

    private String createBy;
    /**
     * 创建时间
     */

    private LocalDateTime createTime;
    /**
     * 更新时间
     */

    private LocalDateTime updateTime;
    /**
     * 操作IP地址
     */

    private String remoteAddr;
    /**
     * 用户代理
     */

    private String userAgent;
    /**
     * 请求URI
     */

    private String requestUri;
    /**
     * 操作方式
     */

    private String method;
    /**
     * 操作提交的数据
     */

    private String params;
    /**
     * 执行时间
     */

    private Long time;

    /**
     * 异常信息
     */

    private String exception;

    /**
     * 服务ID
     */

    private String serviceId;

    /**
     * 删除标记
     */

    @TableLogic
    private String delFlag;


}

接下来创建日志事件类 SysLogEvent,他的参数就是日志类SysLog :

1
2
3
4
5
6
7
8
9
10
/**
 * SysLog事件类
 * @author lzyz.fun
 */

public class SysLogEvent extends ApplicationEvent {

    public SysLogEvent(SysLog source) {
        super(source);
    }
}

有了事件,就可以创建事件监听类SysLogListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * SysLog监听
 * 所有被注解@setLog标注的方法,都会被监听,同时异步入库
 * @author lzyz.fun
 */

@Slf4j
@Component
public class SysLogListener {

    @Resource
    private RemoteLogService remoteLogService; // Feign调用日志保存接口

    /**
     * 本系统日志记录
     * @param event
     */

    @Async
    @Order
    @EventListener(SysLogEvent.class)
    public void saveSysLog(SysLogEvent event) {
        SysLog sysLog = (SysLog) event.getSource();
        remoteLogService.saveLog(sysLog, SecurityConstants.FROM_IN);
    }


    /**
     * 第三方日志平台 - webapi推送
     * @param event
     */

    @Async
    @EventListener(SysLogEvent.class)
    public void saveLog_4FSSS(SysLogEvent event) {
        SysLog sysLog = (SysLog) event.getSource();
        remoteLogService.setLog_4FSSS( sysLog );
    }

    // 后续可继续添加其他平台日志规则。
    // 在 SysLogService 接口新增对应平台的接入逻辑。
    // 最后使用remoteLogService调用即可。
}

remoteLogService.saveLog(sysLog, SecurityConstants.FROM_IN); 是一个feign调用,调用了主服务保存日志的接口, 即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@FeignClient(value = ServiceNameConstants.META_SERVICE, fallbackFactory = RemoteLogServiceFallbackFactory.class)
public interface RemoteLogService {
    /**
     * 保存日志
     * @param sysLog 日志实体
     * @param from   内部调用标志
     * @return succes、false
     */

    @PostMapping("/log/setLog")
    R<Boolean> saveLog(@RequestBody SysLog sysLog, @RequestHeader(SecurityConstants.FROM) String from);

    /**
     * 保存日志_三方平台
     *
     * @param sysLog 日志实体
     * @return succes、false
     */

    @PostMapping("/log/setLog_4FSSS")
    JSONObject setLog_4FSSS(@RequestBody SysLog sysLog );
}

事件和监听都有了。创建自定义日志注解SetLog和注解的AOP拦截。然后在需要被记录的controller方法上使用此注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 日志监听注解
 * 所有被此注解标注的方法均会生成操作日志信息。
 * @author lzyz.fun
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SetLog {

    /**
     * 日志描述标题
     *
     * @return {String}
     */

    String value();
}

创建注解SetLog 拦截 SysLogAspect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * 监听SetLog注解。
 * @author lzyz.fun
 */

@Aspect
@Slf4j
@Component
public class SysLogAspect {

    @Resource
    private RedisUtils  redisUtils;
    // 1.自动注入ApplicationEventPublisher
    @Autowired
    private ApplicationEventPublisher publisher;

    @Pointcut("@annotation(fun.lzyz.common.log.annotation.SetLog)")
    private void logMethod() {}

    @SneakyThrows
    @Around("logMethod() && @annotation(setlog)")
    public Object around( ProceedingJoinPoint point, SetLog setlog ) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String strClassName        = point.getTarget().getClass().getName();
        String strMethodName       = point.getSignature().getName();
        log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
        // 记录执行时长
        Long startTime = System.currentTimeMillis();
        Object obj = point.proceed();
        Long endTime = System.currentTimeMillis();
        // 生成类
        SysLog logVo = createSysLog( point, request );
        logVo.setTitle(setlog.value());
        logVo.setTime(endTime - startTime);
        // 关键点:使用Spring自带的publisher触发日志监听事件
        publisher.publishEvent(new SysLogEvent(logVo));
        return obj;
    }


    /**
     * 创建SysLog类记录本系统日志
     * @param point    ProceedingJoinPoint
     * @param request  HttpServletRequest
     * @return  SysLog
     */

    private SysLog createSysLog( ProceedingJoinPoint point,  HttpServletRequest request ){
        SysLog sysLog = new SysLog();
        String method = request.getMethod();
        String token  = request.getHeader("Authorization");

        Map<Object, Object> hmget = redisUtils.hmget(CommonConstants.REDIS_USER_KEY, token);
        String name = (String) hmget.get("UserName");
        sysLog.setCreateBy(name );
        sysLog.setType(CommonConstants.STATUS_NORMAL);
        sysLog.setRemoteAddr(ServletUtil.getClientIP(request));
        sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        sysLog.setMethod(method);
        sysLog.setUserAgent(request.getHeader("user-agent"));
        if( "get".equalsIgnoreCase(method) ){
            sysLog.setParams(HttpUtil.toParams(request.getParameterMap()));
        }
        if("post".equalsIgnoreCase( method )){
            for (Object obj : point.getArgs()) {
                JSONObject ja = new JSONObject(obj);
                sysLog.setParams(ja.toJSONString(0));
            }
        }
        return sysLog;
    }
}

至此日志模块完成。 接下来看如何使用此注解。

3、使用

只要在其他服务中引入了基础模块core-syslog ,然后在对应controller的方法上贴此注解即可,如:

1
2
3
4
5
    @SetLog("修改信息")
    @PostMapping("/update")
    public JSONObject updateDataBase ( @RequestBody Data dt ) {
         return service.updateData(dt);
    }

这样,只要有请求入口被贴了@SetLog注解,就可以被识别并记录日志。 这样既不会和业务逻辑代码有过深的耦合,阅读和修改起来也十分方便。

评论

目前还没有评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注