若依的日志记录系统AOP加线程队列处理思路

今天改造项目的时候,重新梳理了一遍日志的处理,发现挺有特点,于是记录下来。
首先设计一个注解,走方法的时候都会被注解切面拦截

/**
     * 新增级联删除配置
     */
    @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();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值