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

一、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。

在这里插入图片描述

– 求知若饥,虚心若愚。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/367974.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue】mvc,mvp,mvvm 的区别

总结&#xff1a; MVC : view 视图层依靠 model 来进行更新渲染&#xff0c;当数据发生改变时。第一步: 通知 controller 进行更新&#xff0c;然后第二步 controller 再通知 model 进行更新。最后 model 才将 view 更新 , 这样大量的逻辑以及更新操作&#xff0c;都需要在con…

2024.2.3每日一题

LeetCode 石子游戏 VII 1690. 石子游戏 VII - 力扣&#xff08;LeetCode&#xff09; 题目描述 石子游戏中&#xff0c;爱丽丝和鲍勃轮流进行自己的回合&#xff0c;爱丽丝先开始 。 有 n 块石子排成一排。每个玩家的回合中&#xff0c;可以从行中 移除 最左边的石头或最右…

【已更新】2024美赛C题代码教学思路数据处理数学建模分析Momentum in Tennis

问题一完整的代码已给出&#xff0c;预计2号晚上或者3号凌晨全部给出。 代码逻辑如下&#xff1a; C题第一问要求我们开发一个模型&#xff0c;捕捉得分时的比赛流程&#xff0c;并将其应用于一场或多场比赛。你的模型应该确定哪名球员在比赛的特定时间表现得更好&#xff0c;…

C系列-动态内存管理

&#x1f308;个人主页: 会编程的果子君 ​&#x1f4ab;个人格言:“成为自己未来的主人~” 目录 为什么要有动态内存分配 malloc和free malloc free calloc和realloc calloc realloc 常见的动态内存的错误 对NULL指针的解引用操作 ​编辑 对动态开辟空间的越界访问…

Three.js学习3:第一个Three.js页面

一、一图看懂Three.js 坐标 这个没什么好说的&#xff0c;只是需要注意颜色。在 Three.js 提供的编辑器中&#xff0c;各种物体的坐标也这样的色彩&#xff1a; 红色&#xff1a;x 轴 绿色&#xff1a;y 轴 蓝色&#xff1a;z 轴 Three.js 提供的编辑器可以在本地 Three.js …

python算法与数据结构(搜索算法和拓扑排序算法)---广度优先搜索和拓扑排序

广度优先搜索BFS 定义&基本内容 广度优先是按照层次由近及远的进行搜索&#xff0c;在当前层次所有可及节点都搜索完毕后才会继续往下搜索&#xff0c;其本质就是寻找从起点到终点的最短路程。 树的广度优先搜索 树的广度优先遍历&#xff0c;可以看成是层序遍历。 访问…

java数据结构与算法刷题-----LeetCode15. 三数之和

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 解题思路 和LeetCode1.两数之和一样&#xff0c;但是这道题边界条件更多。…

基于协同过滤的个性化电影推荐系统分析设计python+flask

本系统为用户而设计制作个性化电影推荐管理&#xff0c;旨在实现个性化电影推荐智能化、现代化管理。本个性化电影推荐自动化系统的开发和研制的最终目的是将个性化电影推荐的运作模式从手工记录数据转变为网络信息查询管理&#xff0c;从而为现代管理人员的使用提供更多的便利…

2401Idea用GradleKotlin编译Java控制台中文出乱码解决

解决方法 解决方法1 在项目 build.gradle.kts 文件中加入 tasks.withType<JavaCompile> {options.encoding "UTF-8" } tasks.withType<JavaExec> {systemProperty("file.encoding", "utf-8") }经测试, 只加 tasks.withType<…

谷粒商城【成神路】-【4】——分类维护

目录 1.删除功能的实现 2.新增功能的实现 3.修改功能的实现 4.拖拽功能 1.删除功能的实现 1.1逻辑删除 逻辑删除&#xff1a;不删除数据库中真实的数据&#xff0c;用指定字段&#xff0c;显示的表示是否删除 1.在application.yml中加入配置 mybatis-plus:global-config:…

俩种方法解决 VScode中 NPM 脚本消失,NPM 脚本未显示在资源管理器侧栏中

npm脚本是npm包管理器的一个功能&#xff0c;允许开发者在package.json文件中定义一系列命令脚本&#xff0c;用于执行各种开发任务。 今天打开准备运行的时候发现找不到NPM脚本了&#xff0c;左侧的一栏完全没有显示&#xff0c;在网上查阅了很多资料后总结出俩个方法可以用来…

寒假作业2月3号

第二章 引用内联重载 一&#xff0e;选择题 1、适宜采用inline定义函数情况是&#xff08;C&#xff09; A. 函数体含有循环语句 B. 函数体含有递归语句 C. 函数代码少、频繁调用 D. 函数代码多、不常调用 2、假定一个函数为A(int i4, int j0) {;}, 则执行“A (1);”语句…

解密二进制世界:Hex-Rays IDA Pro forMac/win交互式反汇编工具

在当今数字化时代&#xff0c;软件和硬件的安全性成为了重中之重。为了保护软件和硬件免受黑客和恶意攻击的威胁&#xff0c;人们需要了解和分析代码的内部结构和工作原理。而Hex-Rays IDA Pro作为一款强大的交互式反汇编工具&#xff0c;为安全专业人士提供了解密二进制世界的…

Juc07_乐观锁和悲观锁、公平锁和非公平锁、递归锁(可重入锁)、死锁及排查、自旋锁

1、 乐观锁和悲观锁 ①. 悲观锁(synchronized关键字和Lock的实现类都是悲观锁) 什么是悲观锁&#xff1f;认为自己在使用数据的时候一定有别的线程来修改数据&#xff0c;因此在获取数据的时候会先加锁&#xff0c;确保数据不会被别的线程修改适合写操作多的场景&#xff0c;…

lua 语法介绍与 NGINX lua 高级用法实战操作

文章目录 一、概述二、lua 安装三、lua 语法1&#xff09;lua 数据类型2&#xff09;lua 变量3&#xff09;lua 拼接字符串4&#xff09;lua 循环5&#xff09;lua 函数6&#xff09;lua 条件控制7&#xff09;lua 库模块 四、NGINX lua 高级用法 一、概述 lua是一种轻量小巧的…

【AI绘画UI+Windows部署】Fooocus:Controlnet原作者结合了sd的开源和Midjourney重新设计的UI

代码&#xff1a;https://github.com/lllyasviel/Fooocus windows一键启动包下载&#xff1a;https://github.com/lllyasviel/Fooocus/releases/download/release/Fooocus_win64_2-1-831.7z B站视频教程&#xff1a;AI绘画入门神器&#xff1a;Fooocus | 简化SD流程&#xff0c…

Boosting semantic human matting with coarse annotations

前向推理在modelscope中开源了&#xff0c;但是训练没开源&#xff0c;且是基于TensorFlow的&#xff0c;复现起来是比较麻烦的。 1.Introduction 分割技术主要集中在像素级二元分类&#xff0c;抠图被建模为前景图像F和背景图像B的加权融合&#xff0c;大多数matte方法采用指…

不做中位剧的腾讯,能靠精品撑起长视频会员吗?

回顾腾讯视频的2023年,马化腾用“厚积薄发”来形容。 在腾讯年会上,马化腾回顾了过去一年长视频业务板块的发展情况,同时也清晰地提出了未来的规划,总结来说就是以下三点: 1、《繁花》《三体》《漫长的季节》是腾讯过去一年特别出彩的剧集,几部大剧撑起了长视频会员业务…

设计模式——2_1 命令(Command)

文章目录 定义图纸一个例子&#xff1a;空调和他的遥控器只有控制面板的空调遥控器可以撤销的操作 碎碎念命令和Runnable命令和事务 定义 把请求封装成一个对象&#xff0c;从而使你可以用不同的请求对客户进行参数化&#xff0c;对请求排队或记录请求日志&#xff0c;以及支持…

Spring Bean 生命周期常见错误

虽然说 Spring 容器上手简单&#xff0c;可以仅仅通过学习一些有限的注解&#xff0c;即可达到快速使用的目的。但在工程实践中&#xff0c;我们依然会从中发现一些常见的错误。尤其当你对 Spring 的生命周期还没有深入了解时&#xff0c;类初始化及销毁过程中潜在的约定就不会…