
一、声明日志体系1、创建 LogOperation注解开关都在注解上切面按配置决定记什么package com.linzhixin.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) // 运行时仍可见AOP才能通过反射读到 public interface LogOperation { /** * 操作描述 用于标识业务的含义 接口名称 */ String value() default ; /** * 是否需要记录请求参数 */ boolean logRequest() default true; /** * 是否需要记录响应参数 */ boolean logResponse() default true; /** * 是否记录接口的调用耗时 */ boolean logExecutionTime() default true; }2、创建 LogOperationAspect 切面这是真正干活的类。AspectComponent让 Spring 把它注册成 Bean并对匹配方法织入通知。package com.linzhixin.common.aspect; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.linzhixin.common.annotation.LogOperation; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Method; Aspect Component Slf4j public class LogOperationAspect { /** * 用于序列话请求/响应的 Jackson 工具 */ private final ObjectMapper objectMapper; public LogOperationAspect(ObjectMapper objectMapper) { this.objectMapper objectMapper; // 配置 Jackson 库的配置当序列化一个空对象没有任何 getter 方法或属性的对象时不要抛出异常而是正常序列化为 {} this.objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); } /** * 环绕通知 */ Around(annotation(com.linzhixin.common.annotation.LogOperation)) public Object logOperation(ProceedingJoinPoint joinPoint) { // 1、访问的是哪个方法或者接口 Long startTime System.currentTimeMillis(); // 获取签名 // getSignature() 方法返回一个 Signature 对象 //MethodSignature 是 Signature 的子接口专门用于描述方法的签名信息。它包含了方法的所有静态元数据是 AOP 中最常用的签名类型。 MethodSignature signature (MethodSignature) joinPoint.getSignature(); //从 MethodSignature 中获取 Java 反射的 Method 对象, Method method signature.getMethod(); //从 Method 对象中提取 LogOperation 注解的实例 LogOperation logOperation method.getAnnotation(LogOperation.class); // 通过 Java 反射获取当前执行方法的方法名 String methodName method.getName(); //如果注解中指定了操作名称就使用注解的值否则使用方法名作为默认值。 String operation logOperation.value().isEmpty() ? methodName : logOperation.value(); // 2、获取请求参数 //从 Spring 的请求上下文 中获取当前 HTTP 请求的属性 // RequestContextHolder(请求上下文持有者): Spring 提供的线程安全的请求上下文容器,一个全局访问点存储当前线程的请求上下文。 // ServletRequestAttributes(Servlet 请求属性): 请求属性的载体包含了当前请求的所有 Servlet 相关信息。 ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 获取HTTP 请求头 本身。它代表着一次具体的网络请求包含传输层的数据URL、IP、Header、Cookie、请求流。 HttpServletRequest request attributes ! null ? attributes.getRequest() : null; // 3、记录请求参数 if(logOperation.logRequest()) { logRequest(operation, joinPoint, request); } // 4、记录响应参数 try { Object result joinPoint.proceed(); if(logOperation.logResponse()) { logResponse(operation, result, startTime, logOperation.logExecutionTime()); } // 5、响应的结果需要返回去 return result; } catch (Throwable e) { throw new RuntimeException(e); } } /** * 记录请求参数 * param operation * param joinPoint * param request */ private void logRequest(String operation, ProceedingJoinPoint joinPoint, HttpServletRequest request) { StringBuilder logBuilder new StringBuilder(); // 1、加上了请求描述 logBuilder.append([).append(operation).append(] 请求参数: ); // 2、加上访问路由和访问方法 if(request ! null) { logBuilder.append(method).append(request.getMethod()) .append(, url).append(request.getRequestURI()); // 3、加上请求参数 String queryString request.getQueryString(); if(queryString ! null !queryString.isEmpty()) { logBuilder.append(, query).append(maskSensitiveData(queryString)); } String authorization request.getHeader(Authorization); if(authorization ! null) { logBuilder.append(, authorization).append(maskToken(authorization)); } } // 4、记录方法参数 Object[] args joinPoint.getArgs(); if(args ! null args.length 0) { logBuilder.append(, args[); // 遍历参数列表 for (int i 0; i args.length; i) { if(i 0) { logBuilder.append(, ); } Object arg args[i]; if(arg null) { logBuilder.append(null); } else if(isFile(arg)) { // 处理文件 logBuilder.append(name).append(getFileName(arg)); } else if(isHttpServletResponse(arg)) { logBuilder.append(下载内容无法打印); } else { try { String argStr maskSensitiveData(objectMapper.writeValueAsString(arg)); logBuilder.append(argStr); } catch (JsonProcessingException e) { logBuilder.append(获取参数异常); } } } logBuilder.append(]); } log.info(logBuilder.toString()); } /** * 记录响应参数的日志 * param operation 操作描述 * param result 执行结果 * param startTime 起始时间戳 * param logTime 是否记录耗时 */ private void logResponse(String operation, Object result, long startTime, boolean logTime) { StringBuilder logBuilder new StringBuilder(); logBuilder.append([).append(operation).append(] 响应结果); if(result null) { logBuilder.append(null); } else { // 序列化响应结果 try { String resultString maskSensitiveData(objectMapper.writeValueAsString(result)); logBuilder.append(resultString); } catch (JsonProcessingException e) { logBuilder.append(响应有异常); } } // 加上访问耗时 if (logTime) { logBuilder.append( 访问耗时).append(System.currentTimeMillis() - startTime).append(ms); } log.info(logBuilder.toString()); } /** * 关键字段脱敏操作 * param data 原始字符串 * return 脱敏后的字符串 */ private String maskSensitiveData(String data) { // 脱敏密码字段 data data.replaceAll(\password\\\s*:\\s*\[^\]*\, \password\:\***\); data data.replaceAll(\passwordHash\\\s*:\\s*\[^\]*\, \passwordHash\:\***\); // 脱敏 token z字段 data data.replaceAll(\token\\\s*:\\s*\[^\]*\, \token\:\***\); data data.replaceAll(\authorization\\\s*:\\s*\[^\]*\, \authorization\:\***\); // 脱敏邮箱 data data.replaceAll(\email\\\s*:\\s*\([^\]*[^\]*)\, \email\:\***\); return data; } /** * 令牌脱敏 * param token 原始令牌 * return 脱敏后的 Token */ private String maskToken(String token) { return ****; } /** * 根据反射判断对象是否是文件 * param object 文件对象 * return 是或者否 */ private boolean isFile(Object object) { //object.getClass() 得到的是该对象在运行时的真正类型信息即 ClassT 对象。 return object ! null object.getClass().getName().contains(MultipartFile); } /** * 根据反射靠对象来复原文件名 * param object 文件对象 * return 文件名 */ private String getFileName(Object object) { try { //object.getClass()获取对象的运行时 Class //.getMethod(getOriginalFilename)查找这个类中名为 getOriginalFilename 的公共方法 Method method object.getClass().getMethod(getOriginalFilename); // String fileName (String) method.invoke(object); // method.invoke(object)执行 object 上的那个方法即 getOriginalFilename // 返回一个 Object 类型的结果 return (String) method.invoke(object); } catch (Exception e) { return 文件未知!; } } /** * 根据反射来判断是否需要打印对象参数 * param object 入参 * return */ private boolean isHttpServletResponse(Object object) { return object ! null object.getClass().getName().contains(HttpServletResponse); } }二、通用响应package com.linzhixin.common.util; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; Data NoArgsConstructor AllArgsConstructor public class Result T { /** * 响应码 */ private Integer code; /** * 响应文案 */ private String message; /** * 响应数据 */ private T data; // 成功、客户端报错、服务端报错 /** * 状态码枚举 */ public enum ResultCode { // 请求成功 SUCCESS(200, 操作成功), // 客户端报错 BAD_REQUEST(400, 请求参数错误), UNAUTHORIZED(401, 未授权), FORBIDDEN(403, 禁止访问), NOT_FOUND(404, 资源不存在), // 服务端报错 SERVER_ERROR(500, 服务报错), GATEWAY_ERROR(502, 网关报错), SERVER_UNAVAILABLE(503, 服务不可用) ; //枚举enum在 Java 中代表一组固定的、不可变的常量实例。既然枚举实例本身是固定的那么它们的属性值也应该是固定的。 private final Integer code; private final String message; ResultCode(Integer code, String message) { this.code code; this.message message; } public Integer getCode() { return code; } public String getMessage() { return message; } } public static T ResultT success(String message, T data) { return new Result(ResultCode.SUCCESS.getCode(), message, data); } /** * 自定义失败响应 * param resultCode 错误枚举 * param message 响应文案 * return 通用响应对象 * param T 泛型 */ public static T ResultT error(ResultCode resultCode, String message) { return new Result(resultCode.getCode(), message, null); } /** * 请求参数报错 * param message 响应文案 * return 通用响应对象 * param T 泛型 */ public static T ResultT badRequest(String message) { return error(ResultCode.BAD_REQUEST, message); } public static T Result T serverError(String message) { return error(ResultCode.SERVER_ERROR, message); } }三、全局异常处理package com.linzhixin.common.config; import com.linzhixin.common.util.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MaxUploadSizeExceededException; /** * 全局异常处理器 */ // ControllerAdvice ResponseBody // ControllerAdvice 是 Spring 框架中的一个全局增强注解用于为所有 Controller 提供统一的增强处理。可以把它理解为一个全局拦截器/增强器作用于整个应用的所有 Controller。 RestControllerAdvice Slf4j public class GlobalExceptionHandler { /** * 请求体不可读异常 * param e 异常内容 * return 客户端 400 报错 */ ExceptionHandler(HttpMessageNotReadableException.class) public ResultVoid handleJsonException(HttpMessageNotReadableException e) { return Result.badRequest(请求格式错误); } /** * 专门处理参数校验失败异常 * param e 异常内容 * return 客户端 400 报错 */ ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleMethodArgsNotValid(MethodArgumentNotValidException e) { return Result.badRequest(请求参数缺失); } /** * 非法参数异常 * param e 异常内容 * return 客户端 400 报错 */ ExceptionHandler(IllegalArgumentException.class) public ResultVoid illegalArgumentException(IllegalArgumentException e) { return Result.badRequest(e.getMessage()); } /** * 上传文件超大异常 * param e 异常内容 * return 客户端 400 报错 */ ExceptionHandler(MaxUploadSizeExceededException.class) public ResultVoid uploadSizeExceeded(MaxUploadSizeExceededException e) { log.warn(文件有点大:{}, e.getMessage()); return Result.badRequest(文件大小超过限制); } /** * Java堆内存溢出 * param e 错误 * return 服务端500报错 */ ExceptionHandler(OutOfMemoryError.class) public ResultVoid oomException(OutOfMemoryError e) { log.error(Java堆内存溢出{}, e.getMessage()); return Result.serverError(系统内存不足); } /** * 兜底异常 * param e 执行异常 * return 服务端500报错 */ ExceptionHandler(RuntimeException.class) public ResultVoid handleRuntimeException(RuntimeException e) { log.error(业务运行异常:{}, e.getMessage()); return Result.serverError(e.getMessage()); } }四、设置统一分页配置package com.linzhixin.common.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; /** * MybatisPlus 核心配置 */ Configuration public class MybatisPlusConfig { /** * 分页插件 */ Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor new MybatisPlusInterceptor(); // 添加分页插件 PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); // 单次返回最大记录数 paginationInnerInterceptor.setMaxLimit(500L); // 请求页码超过总页数返回空 paginationInnerInterceptor.setOverflow(false); mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor); return mybatisPlusInterceptor; } }