UT010004: Cannot call getReader(), getInputStream() already called 解决方案

200 阅读1分钟

问题描述

遇到一个需求: 在过滤器中进行请求参数进行记录。

但是发现如果使用 getReader() or getInputStream() 方法获取到POST请求的请求体后, 后续再到Controller层中使用 @RequestBody 注解就会报错:

java.lang.IllegalStateException: UT010004: Cannot call getReader(), getInputStream() already called

问题原因

ServletRequest 的设计限制了请求体只能被读取一次。一旦调用了 getInputStream() 或 getReader(),再次调用这些方法就会抛出异常。@RequestBody 解析参数时也会调用 上面两个方法

解决方案

Step1 : 自定义 CachedBodyHttpServletRequest 缓存请求体

package com.cyou.common.web.filter;
​
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
​
import java.io.ByteArrayInputStream;
import java.io.IOException;
​
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    private final byte[] cachedBody;
​
    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        cachedBody = request.getInputStream().readAllBytes();
    }
​
    @Override
    public ServletInputStream getInputStream() {
        return new ServletInputStream() {
            private final ByteArrayInputStream buffer = new ByteArrayInputStream(cachedBody);
​
            @Override
            public int read() {
                return buffer.read();
            }
​
            @Override
            public boolean isFinished() {
                return buffer.available() == 0;
            }
​
            @Override
            public boolean isReady() {
                return true;
            }
​
            @Override
            public void setReadListener(ReadListener listener) {
                throw new UnsupportedOperationException();
            }
        };
    }
​
    public byte[] getCachedBody() {
        return cachedBody;
    }
}
​

Step2 : 使用 CachedBodyHttpServletRequest缓存请求,将请求在链中传递下去

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
​
public class CachedBodyFilter implements Filter {
​
    @Override
    public void init(FilterConfig filterConfig) {
        // 初始化方法(如果需要)
    }
​
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
            // todo 进行 POST 请求的请求体获取
            String body = new String(cachedRequest.getCachedBody(), request.getCharacterEncoding());
            // 使用缓存请求继续传递
            chain.doFilter(cachedRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }
​
    @Override
    public void destroy() {
        // 销毁方法(如果需要)
    }
}
​

Step3 : 这样就能使用@RequestBody 解析请求体了

import org.springframework.web.bind.annotation.*;
​
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
​
@RestController
@RequestMapping("/api")
public class ApiController {
​
    @PostMapping("/process")
    public Map<String, Object> processRequest(@RequestBody Map<String, Object> body) {
        // 直接使用 @RequestBody 获取请求体
        System.out.println("Controller received body: " + body);
        return body;
    }
}
​