Appearance
@SysLog 记录操作日志
- 配置文件介绍
yaml
acuity:
log:
enabled: false # false=禁止记录操作日志 true=开启
type: DB # 日志存储位置 DB=数据库 (base_operation_log) LOGGER=日志文件
acuity:
log:
enabled: false # false=禁止记录操作日志 true=开启
type: DB # 日志存储位置 DB=数据库 (base_operation_log) LOGGER=日志文件
- 注解属性介绍详看
@WebLog
中的注释! 其中value属性是支持SpEL表达式的. - 注解使用举例
java
// 禁用操作日志
@WebLog(enabled = false)
// response = false不记录方法的响应参数
@WebLog(value = "'分页列表查询:第' + #params?.current + '页, 显示' + #params?.size + '行'", response = false)
// requestByError = false 表示, 方法报错时, 也不记录请求参数
@WebLog(value = "'保存订单:订单编码' + #data?.code + ', 订单Id:' + #data?.name", request = false, requestByError = false)
// 禁用操作日志
@WebLog(enabled = false)
// response = false不记录方法的响应参数
@WebLog(value = "'分页列表查询:第' + #params?.current + '页, 显示' + #params?.size + '行'", response = false)
// requestByError = false 表示, 方法报错时, 也不记录请求参数
@WebLog(value = "'保存订单:订单编码' + #data?.code + ', 订单Id:' + #data?.name", request = false, requestByError = false)
- WebLog 注解参数详解
java
public @interface WebLog {
/**
* 是否启用 操作日志
* 禁用控制优先级:acuity.log.enabled = false > 控制器类上@SysLog(enabled = false) > 控制器方法上@SysLog(enabled = false)
*
* @return 是否启用
*/
boolean enabled() default true;
/**
* 操作日志的描述, 支持spring 的 SpEL 表达式。
*
* @return {String}
*/
String value() default "";
/** 模块 */
String modular() default "";
/**
* 是否拼接Controller类上@Api注解的描述值
*
* @return 是否拼接Controller类上的描述值
*/
boolean controllerApiValue() default true;
/**
* 是否记录方法的入参
*
* @return 是否记录方法的入参
*/
boolean request() default true;
/**
* 若设置了 request = false、requestByError = true,则方法报错时,依然记录请求的入参
*
* @return 当 request = false时, 方法报错记录请求参数
*/
boolean requestByError() default true;
/**
* 是否记录返回值
*
* @return 是否记录返回值
*/
boolean response() default true;
}
public @interface WebLog {
/**
* 是否启用 操作日志
* 禁用控制优先级:acuity.log.enabled = false > 控制器类上@SysLog(enabled = false) > 控制器方法上@SysLog(enabled = false)
*
* @return 是否启用
*/
boolean enabled() default true;
/**
* 操作日志的描述, 支持spring 的 SpEL 表达式。
*
* @return {String}
*/
String value() default "";
/** 模块 */
String modular() default "";
/**
* 是否拼接Controller类上@Api注解的描述值
*
* @return 是否拼接Controller类上的描述值
*/
boolean controllerApiValue() default true;
/**
* 是否记录方法的入参
*
* @return 是否记录方法的入参
*/
boolean request() default true;
/**
* 若设置了 request = false、requestByError = true,则方法报错时,依然记录请求的入参
*
* @return 当 request = false时, 方法报错记录请求参数
*/
boolean requestByError() default true;
/**
* 是否记录返回值
*
* @return 是否记录返回值
*/
boolean response() default true;
}
原理:
说明
通过AOP拦截标记了@WebLog 的方法, 拦截后, SysLogAspect 中的方法用于获取被拦截方法的入参和返回值. 然后发布一个 SysLogEvent 事件 (为了让操作日志尽可能少的影响方法调用时长,采用事件方式来异步处理, 大家也可以改成消息队列之类的), SysLogListener 监听器接收到事件后, 调用consumer.accept 方法, 让对操作日志进行存储.
使用步骤:
在 controller上标记 @WebLog
启动项目,并调用controller方法
AOP切面拦截请求,并记录入参和出参
java@Aspect public class SysLogAspect { @AfterReturning(returning = "ret", pointcut = "sysLogAspect()") public void doAfterReturning(JoinPoint joinPoint, Object ret) { } @Before(value = "sysLogAspect()") public void doBefore(JoinPoint joinPoint) { } }
@Aspect public class SysLogAspect { @AfterReturning(returning = "ret", pointcut = "sysLogAspect()") public void doAfterReturning(JoinPoint joinPoint, Object ret) { } @Before(value = "sysLogAspect()") public void doBefore(JoinPoint joinPoint) { } }
记录好参数后,发布事件待消费者处理参数
javaprivate void publishEvent(OptLogDTO sysLog) { sysLog.setFinishTime(LocalDateTime.now()); sysLog.setConsumingTime(sysLog.getStartTime().until(sysLog.getFinishTime(), ChronoUnit.MILLIS)); SpringUtils.publishEvent(new SysLogEvent(sysLog)); THREAD_LOCAL.remove(); }
private void publishEvent(OptLogDTO sysLog) { sysLog.setFinishTime(LocalDateTime.now()); sysLog.setConsumingTime(sysLog.getStartTime().until(sysLog.getFinishTime(), ChronoUnit.MILLIS)); SpringUtils.publishEvent(new SysLogEvent(sysLog)); THREAD_LOCAL.remove(); }
消费者监听到事件,并将日志存储
javapublic class SysLogListener { private final Consumer<OptLogDTO> consumer; @Async @Order @EventListener(SysLogEvent.class) public void saveSysLog(SysLogEvent event) { OptLogDTO sysLog = (OptLogDTO) event.getSource(); // 非租户模式 (NONE) , 需要修改这里的判断 if (sysLog == null || sysLog.getTenantId() == null) { log.warn("租户编码不存在,忽略操作日志=={}", sysLog != null ? sysLog.getRequestUri() : ""); return; } ContextUtil.setTenantId(sysLog.getTenantId()); // accept 方法是一个回调,具体的存储逻辑需要各个服务自己实现 consumer.accept(sysLog); } }
public class SysLogListener { private final Consumer<OptLogDTO> consumer; @Async @Order @EventListener(SysLogEvent.class) public void saveSysLog(SysLogEvent event) { OptLogDTO sysLog = (OptLogDTO) event.getSource(); // 非租户模式 (NONE) , 需要修改这里的判断 if (sysLog == null || sysLog.getTenantId() == null) { log.warn("租户编码不存在,忽略操作日志=={}", sysLog != null ? sysLog.getRequestUri() : ""); return; } ContextUtil.setTenantId(sysLog.getTenantId()); // accept 方法是一个回调,具体的存储逻辑需要各个服务自己实现 consumer.accept(sysLog); } }
业务服务,根据自己的需求配置日志存储方式
java@Configuration public class BaseWebConfiguration extends BaseConfig { /** * acuity.log.enabled = true 并且 acuity.log.type=DB时实例该类 */ @Bean @ConditionalOnExpression("${acuity.log.enabled:true} && 'DB'.equals('${acuity.log.type:LOGGER}')") public SysLogListener sysLogListener(BaseOperationLogService logApi) { /* accept 方法是一个回调,具体的存储逻辑需要各个服务自己实现 * * SysLogListener中执行 consumer.accept(sysLog); 后,就会执行: * data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class)) */ return new SysLogListener(data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class))); } }
@Configuration public class BaseWebConfiguration extends BaseConfig { /** * acuity.log.enabled = true 并且 acuity.log.type=DB时实例该类 */ @Bean @ConditionalOnExpression("${acuity.log.enabled:true} && 'DB'.equals('${acuity.log.type:LOGGER}')") public SysLogListener sysLogListener(BaseOperationLogService logApi) { /* accept 方法是一个回调,具体的存储逻辑需要各个服务自己实现 * * SysLogListener中执行 consumer.accept(sysLog); 后,就会执行: * data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class)) */ return new SysLogListener(data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class))); } }