今天改造项目的时候,重新梳理了一遍日志的处理,发现挺有特点,于是记录下来。
首先设计一个注解,走方法的时候都会被注解切面拦截
/**
* 新增级联删除配置
*/
@SaCheckPermission("cascadeDelete:cascadeDelete:add")
@Log(title = "级联删除配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody CascadeDeleteBo bo) {
return toAjax(iCascadeDeleteService.insertByBo(bo));
}
注解
package com.haoyu.common.annotation;
import com.haoyu.common.enums.BusinessType;
import com.haoyu.common.enums.OperatorType;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*
* @author ruoyi
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
}
接下来写切面拦截
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// *========数据库日志=========*//
OperLogDTO operLog = new OperLogDTO();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = ServletUtils.getClientIP();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
operLog.setOperName(LoginHelper.getUsername());
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
SpringUtils.getBean(OperLogService.class).recordOper(operLog);
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, OperLogDTO operLog) throws Exception {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
}
}
}
删掉了一些方法,是为了更简洁,然后看这条语句
SpringUtils.getBean(OperLogService.class).recordOper(operLog);
这里调用了一个消费者生产者模型,将对象放入一个线程队列BlockingQueue中,
这个BlockingQueue有什么特点呢?就是任何时候都只能有一个线程能够对这个队列进行put或者get操作。其他的访问时没获得锁会进行等待
这个就是生产者,在生产信息。
@RequiredArgsConstructor
@Service
//@DS("clickhouse")
public class SysOperLogServiceImpl implements ISysOperLogService, OperLogService {
private final SysOperLogMapper baseMapper;
private BlockingQueue<SysOperLog> queue=new LinkedBlockingQueue<>();
@Override
public SysOperLog poll(long timeout,TimeUnit timeUnit) throws InterruptedException {
return queue.poll(timeout, timeUnit);
}
/**
* 操作日志记录
*
* @param operLogDTO 操作日志信息
*/
@Override
public void recordOper(final OperLogDTO operLogDTO) {
SysOperLog operLog = BeanUtil.toBean(operLogDTO, SysOperLog.class);
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
operLog.setOperTime(new Date());
queue.add(operLog);
}
}
但是,借助一句话:
使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。
否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。
所以下面进行消费者的设计
@Slf4j
@RequiredArgsConstructor
@Component
public class SysOperLogConsumer {
private long lastestInsertTime=0;
private final ISysOperLogService sysOperLogService;
public void startInsertFromQueue() {
ThreadUtil.execute(() -> {
while (true) {
List<SysOperLog> list = new ArrayList<>();
try {
while (list.size() < 1000) {
SysOperLog operLog = sysOperLogService.poll(1,TimeUnit.SECONDS);
//最多阻塞1秒,如果没有新数据(operLog == null),也执行一次插入。
if (operLog != null) {
list.add(operLog);
}
if(System.currentTimeMillis()-lastestInsertTime>1000){
batchInsert(list);
}
}
batchInsert(list);
} catch (InterruptedException e) {
batchInsert(list);
throw new RuntimeException(e);
}
}
});
}
private void batchInsert( List<SysOperLog> list){
if (!list.isEmpty()) {
sysOperLogService.insertBatch(list);
lastestInsertTime=System.currentTimeMillis();
list.clear();
}
}
}
这里执行了一个线程,不断while(true)循环,用poll方法从队列里拿取信息。这个方法的特点是拿取了会删掉头部,指定等待时间,如果超时会返回null。
最后进行批量的数据库插入。
这实际上是想实现一种效果,启动线程不断的查询有无需要插入的日志信息,如果有就插入
最后在spring启动的时候启用该消费者,该初始化类继承ApplicationRunner接口
@Override
public void run(ApplicationArguments args) throws Exception {
...
sysOperLogConsumer.startInsertFromQueue();
}