You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:请求体读取为空的修复

请求体读取为空的核心原因有两个:

  1. ContentCachingRequestWrapper不能在AOP切面中直接new了就用,它必须在请求进入Controller之前就完成原生Request的包装,否则Spring MVC已经将请求流读完,后续new的Wrapper根本缓存不到内容。
  2. Servlet请求流只能读取一次,直接调用request.getReader()读取时,流已经被Spring MVC的@RequestBody解析器消费过,自然读出来是空。

修复步骤:

  1. 注册一个最高优先级的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();
        }
    }
}
  1. 切面中不要自己new Wrapper,直接从RequestContextHolder中获取已经被Filter包装过的对象,且不要在@Before通知中取请求体——此时请求还没被完全读取缓存,要在@Around通知中执行完joinPoint.proceed()之后再取缓存内容。

问题2:获取BaseObject内部实际对象的修复

从JoinPoint中拿到的BaseObject的data为null,通常是两个原因:要么切面执行时机太早,参数还没完成反序列化赋值;要么BaseObject没有写getData()/setData()方法,无法访问私有属性data。
正确处理逻辑:

  1. 用@Around通知,在proceed()执行完成后,遍历joinPoint.getArgs()拿到的参数,判断如果是BaseObject类型,直接调用getData()方法即可拿到内部实际请求对象,不需要手动解析泛型。
  2. 响应对象获取更简单,joinPoint.proceed()的返回值就是Controller返回的BaseObject实例,强转后调用getData()就能拿到内部的实际响应对象。
  3. 如果需要原始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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:33:18