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、日志入库等逻辑中。

/**
 * 日志类
 * @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 :

/**
 * SysLog事件类
 * @author lzyz.fun
 */
public class SysLogEvent extends ApplicationEvent {

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

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

/**
 * 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调用,调用了主服务保存日志的接口, 即:

@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方法上使用此注解即可。

/**
 * 日志监听注解
 * 所有被此注解标注的方法均会生成操作日志信息。
 * @author lzyz.fun
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SetLog {

	/**
	 * 日志描述标题
	 *
	 * @return {String}
	 */
	String value();
}

创建注解SetLog 拦截 SysLogAspect:

/**
 * 监听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的方法上贴此注解即可,如:

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

 

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

Comments

No comments yet. Why don’t you start the discussion?

发表回复

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