我的项目使用的是 SpringBoot 3。
要在 Spring Boot 3 项目中使用 AOP(面向切面编程)来打印接收和响应的参数,如 URL、参数、头部信息、请求体等,可以按照以下步骤操作:
步骤 1: 添加依赖
确保你的 pom.xml 文件中包含 spring-boot-starter-aop 依赖。如果你创建的是一个标准的 Spring Boot 项目,这个依赖通常已经包含在内了。如果没有,请手动添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤 2: 创建切面类
创建一个新的类用于定义你的切面逻辑。例如,可以命名为 LoggingAspect.java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
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 jakarta.servlet.http.HttpServletRequest;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* your.package.name.controller..*(..))")
public void logRequest(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 打印URL
System.out.println("URL : " + request.getRequestURL().toString());
// 打印HTTP方法
System.out.println("HTTP Method : " + request.getMethod());
// 打印Header信息
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + " : " + request.getHeader(headerName));
}
// 打印请求参数
System.out.println("Params : " + request.getQueryString());
// 注意:直接从request对象获取body内容比较复杂,因为一旦读取就无法再次读取。
// 对于需要读取body的情况,你可能需要使用HttpServletRequestWrapper或者Filter来实现。
}
@AfterReturning(pointcut = "execution(* your.package.name.controller..*(..))", returning = "result")
public void logResponse(Object result) {
System.out.println("Response Body : " + result);
}
}
请注意,直接从 HttpServletRequest 中读取 body 内容可能会遇到挑战,因为一旦读取后,流将不可再次读取。对于需要访问 body 的场景,你可能需要使用 HttpServletRequestWrapper 或者 Filter 来缓存请求体,以便多次读取。
步骤 3: 配置类(如果需要)
对于某些高级需求,比如需要读取请求体,你可能还需要创建一个配置类或使用过滤器(Filter)来处理请求体的重复读取问题。
以上就是如何在 Spring Boot 3 项目中使用 AOP 打印接收和响应参数的基本步骤。根据实际需要调整包名和类名,并考虑是否需要处理请求体的特殊情形。
没了吗?
别急, 继续挑战 body 输出。接来下要做改造了。
为了在 Spring Boot 应用中使用 AOP 记录请求体(body),同时避免由于 InputStream 只能被读取一次而导致的问题,你可以通过创建一个自定义的 HttpServletRequestWrapper 来缓存请求体。这样,在你的切面逻辑中就可以多次访问请求体了。下面是一个具体的实现示例:
步骤 1: 创建 CachedBodyHttpServletRequest 类
首先,创建一个类继承 HttpServletRequestWrapper,用于缓存请求体。
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* 自定义 HttpServletRequestWrapper 来缓存请求体。
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
/**
* 构造函数,读取并缓存原始请求体。
*/
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = requestInputStream.readAllBytes();
}
/**
* 获取缓存的请求体。
*/
public byte[] getCachedBody() {
return cachedBody;
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
}
步骤 2: 创建 CachedBodyServletInputStream 类
然后,创建一个类来包装原始的 ServletInputStream,以便提供缓存的请求体数据。
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* 自定义 ServletInputStream 来包装缓存的请求体。
*/
public class CachedBodyServletInputStream extends ServletInputStream {
private final ByteArrayInputStream cachedBodyInputStream;
/**
* 使用缓存的请求体构造实例。
*/
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
return this.cachedBodyInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return this.cachedBodyInputStream.read();
}
}
步骤 3: 创建并注册 Filter
接下来,创建一个过滤器来替换原始请求为你的 CachedBodyHttpServletRequest 实例。
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 过滤器用于将原始请求替换为自定义的 CachedBodyHttpServletRequest。
*/
@Component
public class CachingRequestBodyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
CachedBodyHttpServletRequest cachedBodyRequest = new CachedBodyHttpServletRequest(httpRequest);
chain.doFilter(cachedBodyRequest, response);
}
@Override
public void destroy() {}
}
注意:确保你在 Spring Boot 配置中注册这个过滤器,例如通过添加 @Component 注解或者在配置类中进行注册。
步骤 4: 修改 LoggingAspect 类
最后,修改你的 LoggingAspect 类以利用缓存请求体的功能:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 使用 AOP 记录请求的 URL、参数、头部信息以及 body 内容。
*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Before("execution(* com.tylerzhong.web.controller..*(..))")
public void logRequest(JoinPoint joinPoint) throws Exception {
log.info("=======================请求数据start=======================");
// 获取当前请求属性
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 从请求属性中获取 HttpServletRequest
HttpServletRequest request = attributes.getRequest();
if (request instanceof CachedBodyHttpServletRequest) {
CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;
// 打印URL
log.info("请求地址:{}", request.getRequestURL().toString());
// 打印HTTP方法
log.info("请求方式:{}", request.getMethod());
// 打印Header信息
var headerNames = request.getHeaderNames();
log.info("请求头:");
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ":" + request.getHeader(headerName));
}
// 打印请求参数
String queryString = request.getQueryString();
// 此处进行解码处理
String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);
log.info("请求参数:{}", decodedQueryString);
// 获取请求体字节数组
byte[] requestBodyBytes = cachedBodyRequest.getCachedBody();
String rawRequestBody = new String(requestBodyBytes, StandardCharsets.UTF_8);
log.info("请求体:{}", rawRequestBody);
log.info("=======================请求数据end=======================");
}
}
}
@AfterReturning(pointcut = "execution(* com.tylerzhong.web.controller..*(..))", returning = "result")
public void logResponse(Object result) {
log.info("-----------------------响应数据start-----------------------");
log.info("响应体:{}", result);
log.info("-----------------------响应数据end-----------------------");
}
}
代码中的下面这段代码进行解码打印是因为请求参数在传输过程中被进行了 URL 编码(也称为百分号编码)。URL 编码是一种用于将字符转换为可以在 URL 中安全传输的格式的编码方式。例如,中文字符“姓名”和“年龄”会被编码为 %E5%A7%93%E5%90%8D 和 %E5%B9%B4%E9%BE%84。
如果你希望打印出原始的、未经过 URL 编码的请求参数,你需要对这些参数进行解码。Java 提供了 java.net.URLDecoder 类来帮助你完成这个任务。
String queryString = request.getQueryString();
// 此处进行解码处理
String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);
到上面为止,基本可以说完成了AOP切面打印日志的所有内容。
但是我的项目中会上传大量的文件,并且是通过二进制流传递的,所以打印出来的都是二进制数据,输出到控制台会很长,所以我这里需要对它进行 base64 编码输出,但是如果传的 json 数据,我需要输出原始数据,即不进行编码。所以我就想了一个折中的办法,一般上传文件的话,字节数据大小都是几百KB,所以我就判断,如果字节数据的大于100KB就进行 base64 编码输出,小于 100KB 就输出原始数据。这样既可以节省空间,也简化了的输出内容。
所以再进行改造一下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 使用 AOP 记录请求的 URL、参数、头部信息以及 body 内容。
*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {
private static final int MAX_RAW_BODY_SIZE = 102400; // 100KB
@Before("execution(* com.tylerzhong.web.controller..*(..))")
public void logRequest(JoinPoint joinPoint) throws Exception {
log.info("=======================请求数据start=======================");
// 获取当前请求属性
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 从请求属性中获取 HttpServletRequest
HttpServletRequest request = attributes.getRequest();
if (request instanceof CachedBodyHttpServletRequest) {
CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;
// 打印URL
log.info("请求地址:{}", request.getRequestURL().toString());
// 打印HTTP方法
log.info("请求方式:{}", request.getMethod());
// 打印Header信息
var headerNames = request.getHeaderNames();
log.info("请求头:");
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ":" + request.getHeader(headerName));
}
// 打印请求参数
String queryString = request.getQueryString();
String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);
log.info("请求参数:{}", decodedQueryString);
// 获取请求体字节数组
byte[] requestBodyBytes = cachedBodyRequest.getCachedBody();
if (requestBodyBytes.length > MAX_RAW_BODY_SIZE) {
// 如果字节数组长度超过阈值,进行Base64编码并打印
String base64EncodedRequestBody = Base64.getEncoder().encodeToString(requestBodyBytes);
log.info("请求体:{}", base64EncodedRequestBody);
} else {
// 否则直接打印原始内容
String rawRequestBody = new String(requestBodyBytes, StandardCharsets.UTF_8);
log.info("请求体:{}", rawRequestBody);
}
log.info("=======================请求数据end=======================");
}
}
}
@AfterReturning(pointcut = "execution(* com.tylerzhong.web.controller..*(..))", returning = "result")
public void logResponse(Object result) {
log.info("-----------------------响应数据start-----------------------");
log.info("响应体:{}", result);
log.info("-----------------------响应数据end-----------------------");
}
}
请确保替换 com.tylerzhong.web 和 com.tylerzhong.web.controller 为你实际的应用包名和控制器所在的包路径。
以上代码段提供了一个完整的解决方案,用于在 Spring Boot 3 应用中使用 AOP 来记录请求的详细信息,包括 URL、参数、头部信息以及 body 内容。注意,这里假设你已经在项目中正确配置了 Spring AOP 相关依赖,并且你的应用是基于 Spring Boot 构建的。