异步记录用户操作日志

记录用户的操作日志,方便排查问题。

解决方案:

  1. 数据库表设计

    CREATE TABLE `sys_log` (
      `id` int NOT NULL AUTO_INCREMENT COMMENT '日志主键ID',
      `module` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属模块',
      `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '日志标题',
      `user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户ID',
      `user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名称',
      `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号',
      `remote_addr` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求IP',
      `request_uri` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求URI',
      `class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '方法类名',
      `method_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '方法名称',
      `params` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求参数',
      `time` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求耗时',
      `browser` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '浏览器名称',
      `os` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作系统',
      `ex_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '错误类型',
      `ex_msg` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '错误信息',
      `create_time` datetime DEFAULT NULL COMMENT '创建日期',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='日志信息表';
    
  2. 切面注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SysLogAop {
    
        /**
         * 模块名称
         */
        String module() default "";
    
        /**
         * 日志标题
         */
        String title() default "";
    
        /**
         * 排除记录的参数
         */
        String[] excludeParam() default {};
    
        /**
         * 是否记录请求参数
         */
        boolean includeParam() default true;
    
        /**
         * 是否记录返回结果
         */
        boolean includeResult() default false;
    }
    
  3. 切面

    @Aspect
    @Component
    public class SysLogAspect {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(SysLogAspect.class);
        private final ThreadLocal<Long> startTime = new ThreadLocal<>();
        @Autowired
        private AsyncSysLogService asyncSysLogService;
        @Autowired
        private ObjectMapper objectMapper;
    
        @Pointcut("@annotation(sysLogAop)")
        public void logPointcut(SysLogAop sysLogAop) {
    
        }
    
        @Before("logPointcut(sysLogAop)")
        public void before(JoinPoint joinPoint, SysLogAop sysLogAop) {
            startTime.set(System.currentTimeMillis());
        }
    
        @AfterReturning(pointcut = "logPointcut(sysLogAop)", returning = "result")
        public void afterReturning(JoinPoint joinPoint, SysLogAop sysLogAop, Object result) {
            saveLog(joinPoint, sysLogAop, null, result);
        }
    
        @AfterThrowing(pointcut = "logPointcut(sysLogAop)", throwing = "exception")
        public void afterThrowing(JoinPoint joinPoint, SysLogAop sysLogAop, Exception exception) {
            saveLog(joinPoint, sysLogAop, exception, null);
        }
    
        /**
         * 记录操作日志
         *
         * @param joinPoint 切点
         * @param sysLogAop 切面
         * @param exception 异常
         * @param result    返回结果
         */
        private void saveLog(JoinPoint joinPoint, SysLogAop sysLogAop, Exception exception, Object result) {
            try {
                SysLog sysLog = new SysLog();
    
                // 设置模块和标题
                sysLog.setModule(sysLogAop.module());
                sysLog.setTitle(sysLogAop.title());
    
                // 设置用户信息
                SysUser currentUser = getCurrentUser();
                if (currentUser != null) {
                    sysLog.setUserId(currentUser.getUserId());
                    sysLog.setUserName(currentUser.getUserName());
                    sysLog.setMobile(formatMobile(currentUser.getPhonenumber()));
                }
    
                // 获取请求信息
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes != null) {
                    HttpServletRequest request = attributes.getRequest();
                    sysLog.setRemoteAddr(getClientIpAddr(request));
                    sysLog.setRequestUri(request.getRequestURI());
                    sysLog.setBrowser(getBrowser(request));
                    sysLog.setOs(getOs(request));
    
                    // 记录请求参数(完整的JSON格式,支持嵌套对象)
                    if (sysLogAop.includeParam() && joinPoint.getArgs() != null) {
                        String params = getCompleteParamsJson(joinPoint.getArgs(), sysLogAop);
                        sysLog.setParams(params.length() > 2000 ? params.substring(0, 2000) + "...[截断]" : params);
                    }
                }
    
                // 设置方法信息
                String className = joinPoint.getTarget().getClass().getName();
                String methodName = joinPoint.getSignature().getName();
                sysLog.setClassName(className);
                sysLog.setMethodName(methodName);
    
                // 计算执行时间(毫秒)
                Long time = System.currentTimeMillis() - startTime.get();
                sysLog.setTime(String.valueOf(time));
    
                // 处理异常信息
                if (exception != null) {
                    sysLog.setExCode(exception.getClass().getSimpleName());
                    String errorMsg = exception.getMessage();
                    sysLog.setExMsg(errorMsg != null && errorMsg.length() > 2000 ?
                            errorMsg.substring(0, 2000) : errorMsg);
                }
    
                // 设置返回值(完整的JSON格式,支持嵌套对象)
                if (sysLogAop.includeResult() && result != null && exception == null) {
                    try {
                        Object filteredResult = filterSensitiveFields(result, sysLogAop);
                        String resultJson = objectMapper.writeValueAsString(filteredResult);
                        // 限制返回值长度,避免数据过大
                        if (resultJson.length() > 5000) {
                            sysLog.setResult(resultJson.substring(0, 5000) + "...[截断]");
                        } else {
                            sysLog.setResult(resultJson);
                        }
                    } catch (Exception e) {
                        sysLog.setResult("返回值序列化失败:" + e.getMessage());
                        LOGGER.error("返回值序列化失败", e);
                    }
                }
    
                // 设置创建时间
                sysLog.setCreateTime(new Date());
    
                // 异步保存日志
                asyncSysLogService.saveLogAsync(sysLog);
            } catch (Exception e) {
                // 记录日志失败不影响正常业务
                LOGGER.error("记录操作日志失败 {}", e.getMessage());
            } finally {
                // 清理ThreadLocal,防止内存泄漏
                startTime.remove();
            }
        }
    
        /**
         * 获取完整的参数JSON(支持嵌套对象)
         */
        private String getCompleteParamsJson(Object[] args, SysLogAop sysLogAop) {
            try {
                List<Object> filteredArgs = new ArrayList<>();
                for (Object arg : args) {
                    if (shouldSkipParameter(arg)) {
                        filteredArgs.add("[" + arg.getClass().getSimpleName() + "]");
                    } else {
                        filteredArgs.add(filterSensitiveFields(arg, sysLogAop));
                    }
                }
                return objectMapper.writeValueAsString(filteredArgs);
            } catch (Exception e) {
                LOGGER.error("参数序列化失败", e);
                return "参数序列化失败:" + e.getMessage();
            }
        }
    
        /**
         * 检查是否应该跳过某些参数
         */
        private boolean shouldSkipParameter(Object arg) {
            return arg instanceof HttpServletRequest ||
                    arg instanceof HttpServletResponse ||
                    arg instanceof MultipartFile ||
                    (arg instanceof Collection && ((Collection<?>) arg).stream()
                            .anyMatch(item -> item instanceof MultipartFile));
        }
    
        /**
         * 过滤敏感字段(支持递归处理嵌套对象)
         */
        private Object filterSensitiveFields(Object obj, SysLogAop sysLogAop) {
            return filterSensitiveFields(obj, new HashSet<>(), sysLogAop);
        }
    
        /**
         * 过滤敏感字段(递归处理,防止循环引用)
         */
        private Object filterSensitiveFields(Object obj, Set<Object> visited, SysLogAop sysLogAop) {
            if (obj == null) {
                return null;
            }
    
            // 防止循环引用
            if (visited.contains(obj)) {
                return "[循环引用: " + obj.getClass().getSimpleName() + "]";
            }
    
            // 基本类型和字符串直接返回
            if (isBasicType(obj)) {
                return obj;
            }
    
            // Date类型特殊处理
            if (obj instanceof Date) {
                return obj;
            }
    
            // 数组处理
            if (obj.getClass().isArray()) {
                visited.add(obj);
                Object[] array = (Object[]) obj;
                Object[] filteredArray = new Object[array.length];
                for (int i = 0; i < array.length; i++) {
                    filteredArray[i] = filterSensitiveFields(array[i], visited, sysLogAop);
                }
                visited.remove(obj);
                return filteredArray;
            }
    
            // Collection处理
            if (obj instanceof Collection) {
                visited.add(obj);
                Collection<?> collection = (Collection<?>) obj;
                List<Object> filteredList = new ArrayList<>();
                for (Object item : collection) {
                    filteredList.add(filterSensitiveFields(item, visited, sysLogAop));
                }
                visited.remove(obj);
                return filteredList;
            }
    
            // Map处理
            if (obj instanceof Map) {
                visited.add(obj);
                Map<?, ?> map = (Map<?, ?>) obj;
                Map<Object, Object> filteredMap = new LinkedHashMap<>();
                for (Map.Entry<?, ?> entry : map.entrySet()) {
                    String key = String.valueOf(entry.getKey());
                    boolean isSensitive = Arrays.stream(sysLogAop.excludeParam())
                            .anyMatch(prop -> prop.equalsIgnoreCase(key));
                    if (isSensitive) {
                        filteredMap.put(entry.getKey(), "***");
                    } else {
                        filteredMap.put(entry.getKey(), filterSensitiveFields(entry.getValue(), visited, sysLogAop));
                    }
                }
                visited.remove(obj);
                return filteredMap;
            }
    
            // 自定义对象处理
            try {
                visited.add(obj);
    
                // 创建一个Map来存储过滤后的字段,而不是创建新的对象实例
                Map<String, Object> filteredObject = new LinkedHashMap<>();
    
                // 获取所有字段,包括父类字段
                List<Field> allFields = getAllFields(obj.getClass());
    
                for (Field field : allFields) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
    
                    // 跳过一些特殊字段
                    if (fieldName.equals("serialVersionUID") || fieldName.contains("$")) {
                        continue;
                    }
    
                    // 检查是否是敏感字段
                    boolean isSensitive = Arrays.stream(sysLogAop.excludeParam())
                            .anyMatch(prop -> prop.equalsIgnoreCase(fieldName));
    
                    try {
                        Object value = field.get(obj);
                        if (isSensitive && value instanceof String) {
                            filteredObject.put(fieldName, "***");
                        } else {
                            filteredObject.put(fieldName, filterSensitiveFields(value, visited, sysLogAop));
                        }
                    } catch (IllegalAccessException e) {
                        // 如果无法访问字段,记录为无法访问
                        filteredObject.put(fieldName, "[无法访问]");
                    }
                }
    
                visited.remove(obj);
                return filteredObject;
            } catch (Exception e) {
                visited.remove(obj);
                LOGGER.warn("过滤对象字段失败: {}", obj.getClass().getName(), e);
                return "[对象序列化失败: " + obj.getClass().getSimpleName() + "]";
            }
        }
    
        /**
         * 获取所有字段,包括父类字段
         */
        private List<Field> getAllFields(Class<?> clazz) {
            List<Field> allFields = new ArrayList<>();
            while (clazz != null && clazz != Object.class) {
                allFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
                clazz = clazz.getSuperclass();
            }
            return allFields;
        }
    
        /**
         * 判断是否是基本类型
         */
        private boolean isBasicType(Object obj) {
            return obj instanceof String ||
                    obj instanceof Number ||
                    obj instanceof Boolean ||
                    obj instanceof Character ||
                    obj.getClass().isPrimitive() ||
                    obj.getClass().isEnum();
        }
    
        /**
         * 格式化手机号,去掉国际区号
         */
        private String formatMobile(String mobile) {
            if (mobile == null || mobile.isEmpty()) {
                return mobile;
            }
            // 去掉+86前缀
            if (mobile.startsWith("+86")) {
                return mobile.substring(3);
            }
            // 去掉86前缀(没有+号的情况)
            if (mobile.startsWith("86") && mobile.length() > 11) {
                return mobile.substring(2);
            }
    
            return mobile;
        }
    
        /**
         * 获取当前登陆人
         */
        private SysUser getCurrentUser() {
            try {
                LoginUser loginUser = SecurityUtils.getLoginUser();
                if (loginUser == null) {
                    return null;
                }
                return loginUser.getSysUser();
            } catch (Exception e) {
                return null;
            }
        }
    
        /**
         * 获取客户端IP地址
         */
        private String getClientIpAddr(HttpServletRequest request) {
            String ip = request.getHeader("X-Forwarded-For");
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Real-IP");
            }
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            return ip;
        }
    
        /**
         * 获取浏览器信息
         */
        private String getBrowser(HttpServletRequest request) {
            String userAgent = request.getHeader("User-Agent");
            if (userAgent == null) return "Unknown";
            if (userAgent.contains("Chrome")) return "Chrome";
            if (userAgent.contains("Firefox")) return "Firefox";
            if (userAgent.contains("Safari")) return "Safari";
            if (userAgent.contains("Edge")) return "Edge";
            if (userAgent.contains("Opera")) return "Opera";
            return "Other";
        }
    
        /**
         * 获取操作系统信息
         */
        private String getOs(HttpServletRequest request) {
            String userAgent = request.getHeader("User-Agent");
            if (userAgent == null) return "Unknown";
            if (userAgent.contains("Windows")) return "Windows";
            if (userAgent.contains("Mac")) return "macOS";
            if (userAgent.contains("Linux")) return "Linux";
            if (userAgent.contains("Android")) return "Android";
            if (userAgent.contains("iPhone")) return "iOS";
            return "Other";
        }
    }
    
  4. 异步记录操作日志,这里可以使用消息队列或者异步线程,本系统是要把操作日志同步到远程数据仓库,采用异步线程发送http请求的方式

    @Service
    public class AsyncSysLogService {
        private static final Logger LOGGER = LoggerFactory.getLogger(AsyncSysLogService.class);
    
        private ThreadPoolExecutor logThreadPool;
    
        @Autowired
        private RemoteSysLogService remoteSysLogService;
    
        @PostConstruct
        public void init() {
            logThreadPool = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
                    new ThreadFactory() {
                        private int count = 1;
    
                        @Override
                        public Thread newThread(@NonNull Runnable r) {
                            return new Thread(r, "SysLogPool-" + count++);
                        }
                    },
                    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
            );
        }
    
        /**
         * 异步保存操作日志到数据库中
         *
         * @param sysLog 操作日志信息
         */
        public void saveLogAsync(SysLog sysLog) {
            logThreadPool.execute(() -> {
                LOGGER.info("保存操作日志:{}", sysLog);
                try {
                    remoteSysLogService.saveLog(sysLog, SecurityConstants.INNER);
                } catch (Exception e) {
                    LOGGER.error("保存操作日志失败:{}", e.getMessage());
                }
            });
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值