SpringBoot 解决 getReader() has already been called for this request

本文详细阐述了在JavaWeb开发中如何处理GET、POST表单和POSTJSON请求的参数获取问题,特别关注getReader()方法的使用限制。通过创建RepeatableHttpServletRequestWrapper类,解决了在拦截器和Controller中多次读取请求体的问题,确保Controller的正常工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、getReader()问题分析

1、获取请求参数的方式

对于 GET 请求和 POST 表单请求,参数都是包含在 URL 查询字符串中的,因此在拦截器中都可以通过使用 request.getParameter(“paramName”)来获取这些参数。

对于 POST JSON 请求,参数通常包含在请求体中,并且请求的 Content-Type为application/json。在这种情况下,我们需要获取请求体数据。

下面是 GET 请求、POST 表单请求和 POST JSON 请求获取参数的方式:

  • 获取 GET 请求和 POST 表单请求参数:
String paramName = request.getParameter("paramName");
  • 获取 POST JSON 请求参数:
// 获取 POST JSON请求参数
String requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));

注意:POST JSON 请求参数会提示:getReader() has already been called for this request。

这个问题一般出现在使用 ServletInputStream对象的时候。Servlet规范只允许读取请求体一次,如果我们在调用了 getReader()方法读取了请求体之后,又调用了 getInputStream()方法读取请求体,就会出现这个问题,因为 getReader()方法底层也是调用 getInputStream()来实现的。

即多次调用 request.getReader()方法时导致的。比如:拦截器和 Controller控制器(@RequestBody)中多次读取请求体中的数据。

2、getReader()解决方案

解决方案:尽量不要重复读取请求体。如果需要多次读取请求体,可以将请求体缓存起来。我们就需要使用 HttpServletRequestWrapper类来包装 HttpServletRequest对象的类。

具体实现:我们自定义一个 RepeatableHttpServletRequestWrapper类继承 HttpServletRequestWrapper类,把 body 保存在 RepeatableHttpServletRequestWrapper类中,并重写 getReader()和 getInputStream()方法,返回新的流对象。这样就可以绕过Servlet规范的限制,多次读取请求体。

二、解决 getReader()实战

为了在拦截器中获取请求体数据,同时又要保持控制器能够正常使用请求体或实体类接口。在这种情况下,我们自定义 RepeatableHttpServletRequestWrapper类来解决 getReader()这个问题。

1、自定义 RepeatableHttpServletRequestWrapper类

public class RepeatableHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private static final int BUFFER_SIZE = 1024 * 8;
    private byte[] body;

    public RepeatableHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        /**
         * 如果请求是多部分请求,就直接返回,不进行后续的处理。
         * 多部分请求包含了文件数据和其他表单字段数据。
         */
        if (ServletFileUpload.isMultipartContent(request)) {
            return;
        }

        BufferedReader reader = request.getReader();
        try (StringWriter writer = new StringWriter()) {
            int read;
            char[] buf = new char[BUFFER_SIZE];
            while ((read = reader.read(buf)) != -1) {
                writer.write(buf, 0, read);
            }
            this.body = writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8);
        }
    }

    /**
     * 获取请求体数据
     *
     * @return
     */
    public String getBody() {
        return new String(this.body, StandardCharsets.UTF_8);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }

}

ServletFileUpload.isMultipartContent(request)方法源码如下:

在这里插入图片描述

2、自定义过滤器

@Slf4j
public class RequestBodyWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest myRequestWrapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            myRequestWrapper = new RepeatableHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        }
        if (myRequestWrapper == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            log.info("使用可重复读取请求体包装类");
            filterChain.doFilter(myRequestWrapper, servletResponse);
        }
    }

}

3、自定义拦截器

/**
 * HandlerInterceptorAdapter类在 Spring 5.3之后就过时了,推荐使用 HandlerInterceptor类
 */
@Slf4j
public class ApiSignInterceptor implements HandlerInterceptor {

    /**
     * timestamp过期时间,单位:毫秒
     */
    private final static Long TIMESTAMP_EXPIRE_TIME = 1000 * 60 * 5L;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestMethod = request.getMethod();
        log.info("ApiSignInterceptor -> preHandle 请求方式 = {},url = {}, ", requestMethod, request.getRequestURI());

        JSONObject jsonObject = new JSONObject();
        // 获取请求数据
        if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod)) {
            jsonObject = getSignRequestParameter(request);
        } else if (HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
            if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())) {
                /**
                 * Filter包装处理之后,在拦截器中上面两种方式都可以,推荐使用方式2。
                 */
                //try {
                //    // 方式1
                //    String requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
                //    jsonObject = JSON.parseObject(requestBody);
                //} catch (IOException e) {
                //    throw new ApiBaiduException("读取请求体中的数据流异常");
                //}

                // 方式2
                RepeatableHttpServletRequestWrapper requestWrapper = (RepeatableHttpServletRequestWrapper) request;
                String requestBody = requestWrapper.getBody();
                jsonObject = JSON.parseObject(requestBody);
            } else {
                jsonObject = getSignRequestParameter(request);
            }
        }

        // 验签
        verifyRequestSign(jsonObject);

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("ApiSignInterceptor -> postHandle 请求方式 = {},url = {}, ", request.getMethod(), request.getRequestURI());
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }


    /**
     * 校验请求签名
     *
     * @param jsonObject
     */
    private void verifyRequestSign(JSONObject jsonObject) {
        String appId = jsonObject.getString("appId");
        Long timestamp = jsonObject.getLong("timestamp");
        String sign = jsonObject.getString("sign");
        Long now = System.currentTimeMillis();
        if (StringUtils.isBlank(appId)) {
            throw new ApiException("appId 不能为空");
        }
        if (timestamp == null) {
            throw new ApiException("timestamp 不能为空");
        }
        //if ((now - timestamp) > TIMESTAMP_EXPIRE_TIME) {
        //    throw new ApiException("请求时间超过规定范围时间 5分钟");
        //}
        if (StringUtils.isBlank(sign)) {
            throw new ApiException("sign 不能为空");
        }
        String generateSign = ApiSignUtils.generateSign(jsonObject, appSecret);
        log.info("ApiSignInterceptor -> sign = {}, generateSign = {}", sign, generateSign);
        if (!generateSign.equals(sign)) {
            throw new ApiException("签名不匹配");
        }
    }

    /**
     * 获取签名请求参数
     *
     * @param request
     * @return
     */
    private JSONObject getSignRequestParameter(HttpServletRequest request) {
        JSONObject jsonObject = new JSONObject();

        String appId = request.getParameter("appId");
        String timestamp = request.getParameter("timestamp");
        String sign = request.getParameter("sign");

        jsonObject.put("appId", appId);
        jsonObject.put("timestamp", timestamp);
        jsonObject.put("sign", sign);
        return jsonObject;
    }

}

拦截器使用方式:Filter包装处理之后,在拦截器中上面两种方式都可以,推荐使用方式2。

Controller Post json请求还是和之前一样,使用 @RequestBody注解获取请求体数据。

4、Web配置类

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 配置过滤器
     */
    @Bean
    public FilterRegistrationBean<RequestBodyWrapperFilter> addRequestBodyWrapperFilter() {
        FilterRegistrationBean<RequestBodyWrapperFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(getRequestBodyWrapperFilter());
        bean.addUrlPatterns("/*"); // 拦截所有的资源
        //bean.addUrlPatterns(WebConstant.API + "/*"); // 拦截 API所有的资源
        bean.setOrder(1);
        bean.setAsyncSupported(true);
        return bean;
    }

    /**
     * 配置拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getDaemonLoginInterceptor())
                .addPathPatterns(WebConstant.DAEMON + "/**");

        registry.addInterceptor(getApiSignInterceptor())
                .addPathPatterns(WebConstant.API + "/**");
    }

    /**
     * 配置静态资源访问拦截
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }


    /**
     * 配置 CORS跨域问题
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                /**
                 * allowedOrigins("*") // allowedOrigins 是用于显式列出允许的源
                 * allowedOriginPatterns("*")  // allowedOriginPatterns 是用于基于模式来匹配允许的源。
                 * 使用 allowedOriginPatterns 可以更加灵活地配置允许的跨域访问,特别是在需要处理多个类似的源时会更加方便。
                 */
                .allowedOriginPatterns("*")
                .allowedHeaders("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600);
    }


    @Bean
    public RequestBodyWrapperFilter getRequestBodyWrapperFilter() {
        return new RequestBodyWrapperFilter();
    }

    @Bean
    public DaemonLoginInterceptor getDaemonLoginInterceptor() {
        return new DaemonLoginInterceptor();
    }

    @Bean
    public ApiSignInterceptor getApiSignInterceptor() {
        return new ApiSignInterceptor();
    }

}

正常访问 Controller 一切 ok。

在这里插入图片描述

– 求知若饥,虚心若愚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值