一、声明日志体系
1、创建 @LogOperation 注解
开关都在注解上,切面按配置决定记什么
java
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 切面
这是真正干活的类。@Aspect + @Component 让 Spring 把它注册成 Bean,并对匹配方法织入通知。
java
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() 得到的是该对象在运行时的真正类型信息,即 Class<T> 对象。
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");
}
}
二、通用响应
java
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> Result<T> success(String message, T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 自定义失败响应
* @param resultCode 错误枚举
* @param message 响应文案
* @return 通用响应对象
* @param <T> 泛型
*/
public static <T> Result<T> error(ResultCode resultCode, String message) {
return new Result<>(resultCode.getCode(), message, null);
}
/**
* 请求参数报错
* @param message 响应文案
* @return 通用响应对象
* @param <T> 泛型
*/
public static <T> Result<T> badRequest(String message) {
return error(ResultCode.BAD_REQUEST, message);
}
public static <T> Result <T> serverError(String message) {
return error(ResultCode.SERVER_ERROR, message);
}
}
三、全局异常处理
java
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 Result<Void> handleJsonException(HttpMessageNotReadableException e) {
return Result.badRequest("请求格式错误");
}
/**
* 专门处理参数校验失败异常
* @param e 异常内容
* @return 客户端 400 报错
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgsNotValid(MethodArgumentNotValidException e) {
return Result.badRequest("请求参数缺失");
}
/**
* 非法参数异常
* @param e 异常内容
* @return 客户端 400 报错
*/
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> illegalArgumentException(IllegalArgumentException e) {
return Result.badRequest(e.getMessage());
}
/**
* 上传文件超大异常
* @param e 异常内容
* @return 客户端 400 报错
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public Result<Void> uploadSizeExceeded(MaxUploadSizeExceededException e) {
log.warn("文件有点大:{}", e.getMessage());
return Result.badRequest("文件大小超过限制");
}
/**
* Java堆内存溢出
* @param e 错误
* @return 服务端500报错
*/
@ExceptionHandler(OutOfMemoryError.class)
public Result<Void> oomException(OutOfMemoryError e) {
log.error("Java堆内存溢出{}", e.getMessage());
return Result.serverError("系统内存不足");
}
/**
* 兜底异常
* @param e 执行异常
* @return 服务端500报错
*/
@ExceptionHandler(RuntimeException.class)
public Result<Void> handleRuntimeException(RuntimeException e) {
log.error("业务运行异常:{}", e.getMessage());
return Result.serverError(e.getMessage());
}
}
四、设置统一分页配置
java
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;
}
}