首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot实现JSON数据重复读取

SpringBoot实现JSON数据重复读取

作者头像
啃饼思录
发布2023-03-18 16:32:52
1.1K0
发布2023-03-18 16:32:52
举报

写在前面

自从上次更新已经半年有余,这其中经历了很多事情,等后续年终总结的时候和大家好好聊聊。

老规矩,今天还是更新技术文章,说一个近期遇到的尴尬问题。前端传给后端的JSON数据,如果开发者对此进行了拦截并进行了消费,那么后续在controller中就无法再次获取对应数据。原因在于服务端是通过IO流来解析JSON数据,而流是一种特殊的结构,只要读完就没有了,而在某些场景下往往希望可以多次读取。

举一个非常简单的例子,接口幂等性实现,即同一个接口在规定时间内多次接收到相同参数的请求,那么此时需要拒绝这些相同请求。我们在具体实现的时候,可能会先将请求中的参数提取出来,如果参数是JOSN数据,那么由于流已经读取了,因此后续在接口是无法再次获取JSON数据的。

问题再现

第一步,新建一个名为many-json的SpringBoot项目,并在其中新增Web依赖。

第二步,新建一个interceptor包,并在该包内新建一个RequestInterceptor类,这个类需要实现HandlerInterceptor接口并重写其中的preHandle方法:

public class RequestInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(RequestInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String line = request.getReader().readLine();
        logger.info("读取的信息为:{}",line);
        return true;
    }
}

这里我们就简单一些,通过流将请求中的参数打印输出一下,这样流就读完了。

第三步,新建一个config包,并在该包内新建一个MyWebMvcConfig类,这个类需要实现WebMvcConfigurer接口并重写其中的addInterceptors方法:

@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
    }
}

这其实就是注册拦截器,并设置该拦截器对所有请求都进行拦截。

第四步,新建一个controller包,并在该包内新建一个KenBingsController类,然后提供一个名为test的接口,注意该接口中参数通过JSON格式来传递:

@RestController
public class KenBingsController {
    private static final Logger logger = LoggerFactory.getLogger(KenBingsController.class);
    @PostMapping("/test")
    public String test(@RequestBody String message){
        logger.info("用户输入的信息为:{}",message);
        return message;
    }
}

第五步,启动项目进行测试。可以看到当用户访问/test接口的时候,该请求被拦截器所拦截,因此preHandle方法将会执行,输入如下信息:

但是由于我们在test方法的参数中使用了@RequestBody注解,而该注解底层是通过解析IO流来解析JSON数据的,加上我们在拦截器中已经读取了流,因此后续接口中就得不到数据:

可是现在我们希望IO流可以被多次读取,此时该如何操作呢?可以利用装饰者模式对HttpServletRequest进行增强,即拦截HttpServletRequest请求且请求参数为JOSN格式调用新的HttpServletRequest包装类。

装饰者模式对HttpServletRequest进行增强

第一步,新建一个wrapper包,并在该包内新建一个MyRequestWrapper类,这个类需要继承HttpServletRequestWrapper类并重写其中的getInputStream和getReader方法,同时重载一下父类ServletRequestWrapper中有HttpServletRequest和HttpServletResponse对象的构造方法:

/**
 * 自定义请求包装类
 */
public class MyRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] bytes;

    public MyRequestWrapper(HttpServletRequest request,HttpServletResponse response) throws IOException {
        super(request);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        bytes = request.getReader().readLine().getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }

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

            @Override
            public int available() throws IOException {
                return bytes.length;
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

这其实就是自定义了一个新的HttpServletRequest类,并重载了一个包含HttpServletRequest和HttpServletResponse对象的构造方法,目的就是修改请求和响应的字符编码格式以及从IO流出读取数据,然后存入一个字节数组中,并通过重写getInputStream和getReader方法分别从字节数组中获取数据并构造IO流进行返回,这样就实现了IO流的多次读取。

第二步,新建一个filter包,并在该包内新建一个MyRequestFilter类,这个类需要实现Filter接口并重写其中的doFilter方法:

/**
 * 请求拦截器,只有JSON数据才会使用自定义的RequestWrapper
 */
public class MyRequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(servletRequest instanceof HttpServletRequest){
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            if(StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)){
                MyRequestWrapper wrapper = new MyRequestWrapper(request,(HttpServletResponse) servletResponse);
                filterChain.doFilter(wrapper,servletResponse);
                return;
            }
            filterChain.doFilter(request,servletResponse);
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

可以看到这里我们重写了doFilter方法,目的就是判断请求的类型,如果请求是HttpServletRequest且请求数据类型为JSON格式才会调用自定义的MyRequestWrapper,即将HttpServletRequest替换为MyRequestWrapper,走IO流可以多次读取的逻辑,之后让过滤器继续往下执行。

请注意,过滤器最好不要使用@Component注解交由Spring容器来管理,这样会导致每个接口都会被进行过滤,最好是开发者自己手动注册,并且配置过滤的接口。

第三步,在之前定义的MyWebMvcConfig类中将这个自定义的MyRequestFilter过滤器注册进去:

@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
    }

    @Bean
    FilterRegistrationBean<MyRequestFilter> myRequestFilterFilterRegistrationBean(){
        FilterRegistrationBean<MyRequestFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new MyRequestFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

第四步,启动项目进行测试。可以发现现在访问/test接口,Postman会返回正常数据:

查看一下控制台可以看到现在controller中也能获取到JSON数据了:

总结

通过装饰者模式对HttpServletRequest进行增强这一方式可以解决JSON重复读取问题,其本质上是对请求数据格式进行判断。如果是JOSN格式,则自定义HttpServletRequest对象,先将数据从IO流中读取,然后存入一个字节数组中,后续多次读取则是多次读取该字节数组并以IO流形式进行。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-12-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 啃饼随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 问题再现
  • 装饰者模式对HttpServletRequest进行增强
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档