Spring AOP拦截RestController时请求体为空、包装类data为null如何解决
问题背景
业务需要将所有传入Rest Controller的请求、响应数据存入数据库,选择通过Spring AOP实现该能力,但落地时遇到两个核心问题:
- 读取到的请求体为空字符串
- 所有请求、响应都通过Base Object统一包装,AOP仅能识别到外层Base类,无法获取内部泛型对应的实际请求/响应对象
核心代码定义
通用包装类BaseObject
public class BaseObject<T> { private T data; }
Controller方法示例
@PostMapping("/student") public BaseObject<StudentResponse> saveStudent(@RequestBody BaseObject<StudentRequest> studentRequest) { // 业务逻辑省略 }
已尝试方案与现存问题
方案1:通过JoinPoint获取参数+手动读取Request流
private void saveRequestInDatabase(ProceedingJoinPoint joinPoint, long time) { System.out.println("****************Inside saveRequestInDatabase method*****************"); System.out.println(Thread.currentThread().getName()); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); DataBaseRequest dataBaseRequest = new DataBaseRequest(); String className = joinPoint.getTarget().getClass().getName(); String methodName = signature.getName(); dataBaseRequest.setMethod(className + "." + methodName + "()"); // 类名、方法名可正常获取 Object[] args = joinPoint.getArgs(); System.out.println("Object Arguments :- " +joinPoint.getArgs()); // 仅能拿到外层BaseObject实例,内部data属性为null LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = u.getParameterNames(method); System.out.println("Param Names :- "+paramNames); if (args != null && paramNames != null) { String params = ""; for (int i = 0; i < args.length; i++) { params += " " + paramNames[i] + ": " + args[i]; // paramNames可正常拿到studentRequest参数名 // args中的BaseObject实例内部data为null } dataBaseRequest.setParams(params); } HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); ContentCachingRequestWrapper req = new ContentCachingRequestWrapper(request); byte[] requestBody = req.getContentAsByteArray(); String reqBodyStr = new String(requestBody, StandardCharsets.UTF_8); JsonObject jsonObject = new JsonObject(); String reqBody = null; try { reqBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); } catch (IOException e) { e.printStackTrace(); } jsonObject.addProperty("requestBody", reqBody); // 执行时requestBody为空字符串 jsonObject.addProperty("reqBody", reqBodyStr); // 执行时reqBodyStr为空字符串 dataBaseRequest.setEndPoint(request.getServletPath()); // 可正常获取 dataBaseRequest.setOperation(request.getMethod()); // 可正常获取 dataBaseRequestService.saveRequest(dataBaseRequest);// 存入数据库的请求对象仅为{data:null} System.out.println("((((((((((((((((Done saving request)))))))))))))))))))))"); }
方案2:通过@Before切面args绑定参数
@Before("execution(your.package.where.is.endpoint.*.*(..)) && args(reqArgs)")
该方式拿到的reqArgs = {BaseObject@21216},内部data属性依然为null。
目前观察到HttpServletRequest对象中存在名为CachedContent = {ByteArrayOutputStream@13221}的属性,存储了Postman传入的完整JSON内容(包含StudentRequest的所有属性),但不知道如何正确提取。
需要解决的核心问题:当请求、响应都被BaseObject包装时,如何正确获取内部的实际请求对象(StudentRequest)、响应对象(StudentResponse),以及完整的请求体内容。
解决方案
问题1:请求体读取为空的修复
请求体读取为空的核心原因有两个:
ContentCachingRequestWrapper不能在AOP切面中直接new了就用,它必须在请求进入Controller之前就完成原生Request的包装,否则Spring MVC已经将请求流读完,后续new的Wrapper根本缓存不到内容。- Servlet请求流只能读取一次,直接调用
request.getReader()读取时,流已经被Spring MVC的@RequestBody解析器消费过,自然读出来是空。
修复步骤:
- 注册一个最高优先级的Filter,在所有请求进入前就用
ContentCachingRequestWrapper包装原生Request、用ContentCachingResponseWrapper包装原生Response,保证请求、响应体能被缓存:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CachingContentFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); try { filterChain.doFilter(requestWrapper, responseWrapper); } finally { // 响应缓存后必须拷贝回原生响应,否则前端会拿到空响应 responseWrapper.copyBodyToResponse(); } } }
- 切面中不要自己new Wrapper,直接从RequestContextHolder中获取已经被Filter包装过的对象,且不要在
@Before通知中取请求体——此时请求还没被完全读取缓存,要在@Around通知中执行完joinPoint.proceed()之后再取缓存内容。
问题2:获取BaseObject内部实际对象的修复
从JoinPoint中拿到的BaseObject的data为null,通常是两个原因:要么切面执行时机太早,参数还没完成反序列化赋值;要么BaseObject没有写getData()/setData()方法,无法访问私有属性data。
正确处理逻辑:
- 用
@Around通知,在proceed()执行完成后,遍历joinPoint.getArgs()拿到的参数,判断如果是BaseObject类型,直接调用getData()方法即可拿到内部实际请求对象,不需要手动解析泛型。 - 响应对象获取更简单,
joinPoint.proceed()的返回值就是Controller返回的BaseObject实例,强转后调用getData()就能拿到内部的实际响应对象。 - 如果需要原始JSON格式的请求/响应字符串,直接从
ContentCachingRequestWrapper拿缓存的请求字节、从ContentCachingResponseWrapper拿缓存的响应字节转字符串即可,不需要依赖JoinPoint中的参数。
完整可用切面示例
@Aspect @Component public class RequestLogAspect { @Autowired private DataBaseRequestService dataBaseRequestService; @Around("execution(* your.controller.package..*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); Object result = null; try { // 执行目标Controller方法 result = joinPoint.proceed(); return result; } finally { long costTime = System.currentTimeMillis() - startTime; // 从上下文中拿已经被Filter包装过的缓存对象 ContentCachingRequestWrapper requestWrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse( ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(), ContentCachingResponseWrapper.class ); // 提取原始请求体JSON String requestBody = ""; if (requestWrapper != null) { requestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8); } // 提取原始响应体JSON String responseBody = ""; if (responseWrapper != null) { responseBody = new String(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8); } // 提取BaseObject内部实际请求对象 Object actualRequest = null; for (Object arg : joinPoint.getArgs()) { if (arg instanceof BaseObject) { actualRequest = ((BaseObject<?>) arg).getData(); } } // 提取BaseObject内部实际响应对象 Object actualResponse = null; if (result instanceof BaseObject) { actualResponse = ((BaseObject<?>) result).getData(); } // 组装对象存库 DataBaseRequest dataBaseRequest = new DataBaseRequest(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String className = joinPoint.getTarget().getClass().getName(); String methodName = signature.getName(); dataBaseRequest.setMethod(className + "." + methodName + "()"); dataBaseRequest.setEndPoint(request.getServletPath()); dataBaseRequest.setOperation(request.getMethod()); dataBaseRequest.setRequestBody(requestBody); dataBaseRequest.setResponseBody(responseBody); dataBaseRequest.setActualRequestObj(actualRequest); dataBaseRequest.setActualResponseObj(actualResponse); dataBaseRequest.setCostTime(costTime); dataBaseRequestService.saveRequest(dataBaseRequest); } } }
注意提前给BaseObject添加getData()、setData()方法,否则无法访问私有属性data。
内容的提问来源于stack exchange,提问作者MysteriousCoder
相关产品推荐
相关产品推荐

