记录用户的操作日志,方便排查问题。
解决方案:
-
数据库表设计
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='日志信息表';
-
切面注解
@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; }
-
切面
@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"; } }
-
异步记录操作日志,这里可以使用消息队列或者异步线程,本系统是要把操作日志同步到远程数据仓库,采用异步线程发送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()); } }); } }