开发工作中,记录系统日志绝对是必不可少的一环。一般来说,本系统日志记录实现起来非常简单,比如在相关业务代码中执行日志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注解,就可以被识别并记录日志。 这样既不会和业务逻辑代码有过深的耦合,阅读和修改起来也十分方便。