• springboot请求体中的流只能读取一次的问题


    场景交代

    在springboot中添加拦截器进行权限拦截时,需要获取请求参数进行验证。当参数在url后面时(queryString)获取参数进行验证之后程序正常运行。但是,当请求参数在请求体中的时候,通过流的方式将请求体取出参数进行验证之后,发现后续流程抛出错误:

    Required request body is missing ...
    

    经过排查,发现ServletInputStream的流只能读取一次(参考:httpServletRequest中的流只能读取一次的原因)。

    这就是为什么在拦截器中读取消息体之后,controller的@RequestBody注解无法获取参数的原因。

    解决思路

    既然知道了原因,那就可以想到一个大概思路了:可不可以把请求的body流换成可重复读的流?

    答案是可以的。可以通过继承HttpServletRequestWrapper类进行。

    解决方案

    1. 继承HttpServletRequestWrapper

    继承HttpServletRequestWrapper类,将请求体中的流copy一份出来,覆写getInputStream()和getReader()方法供外部使用。如下,每次调用覆写后的getInputStream()方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。

    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    
    import org.springframework.util.StreamUtils;
    
    /**
     * 
     * 从请求体中获取参数请求包装类:<br>
     * @author nick
     * @version 5.0 since 2018年9月5日
     */
    public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{
    	
    	private byte[] requestBody = null;//用于将流保存下来
    
    	public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    		super(request);
    		requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    	}
    
    	@Override
        public ServletInputStream getInputStream() throws IOException {
    
            final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
    
            return new ServletInputStream() {
    
                @Override
                public int read() throws IOException {
                    return bais.read();
                }
    
                @Override
                public boolean isFinished() {
                    return false;
                }
    
                @Override
                public boolean isReady() {
                    return false;
                }
    
                @Override
                public void setReadListener(ReadListener readListener) {
    
                }
            };
        }
    	
    	@Override
    	public BufferedReader getReader() throws IOException{
    		return new BufferedReader(new InputStreamReader(getInputStream()));
    	}
    }
    
    
    2. 替换原始request对象

    现在可重复读取流的请求对象构造好了,但是需要在拦截器中获取,就需要将包装后的请求对象放在拦截器中。由于filter在interceptor之前执行,因此可以通过filter进行实现。

    创建filer,在filter中对request对象用包装后的request替换。

    import java.io.IOException;
    
    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.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    
    import com.znz.dns.controller.interceptor.auth.BodyReaderHttpServletRequestWrapper;
    
    @WebFilter(filterName="bodyReaderFilter",urlPatterns="/*")
    public class BodyReaderFilter implements Filter{
    	
    	@Override
    	public void init(FilterConfig filterConfig) throws ServletException {
    		// do nothing
    	}
     
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    			throws IOException, ServletException {
    		ServletRequest requestWrapper=null;
    		if(request instanceof HttpServletRequest) {
    			requestWrapper=new BodyReaderHttpServletRequestWrapper((HttpServletRequest)request);
    		}
    		if(requestWrapper==null) {
    			chain.doFilter(request, response);
    		}else {
    			chain.doFilter(requestWrapper, response);
    		}
    		
    	}
     
    	@Override
    	public void destroy() {
    		// do nothing
    		
    	}
     
    }
    
    

    配置filter

    @Bean
    public FilterRegistrationBean<BodyReaderFilter> Filters() {
    	FilterRegistrationBean<BodyReaderFilter> registrationBean = new FilterRegistrationBean<BodyReaderFilter>();
    	registrationBean.setFilter(new BodyReaderFilter());
    	registrationBean.addUrlPatterns("/*");
    	registrationBean.setName("koalaSignFilter");
    	return registrationBean;
    }
    
    3. 获取请求体

    既然request对象流已经换成了wrapper reqest,那么流就可以重复读取了。接下来就是获取。

    在拦截器中,直接从request中获取流,并进行读取:

    /**
     * 获取请求体内容
     * @return
     * @throws IOException
     */
    private Map<String, Object> getParamsFromRequestBody(HttpServletRequest request) throws IOException {
    	BufferedReader reader = request.getReader();
    	
    	StringBuilder builder = new StringBuilder();
    	try {
    		String line = null;
    		while((line = reader.readLine()) != null) {
    			builder.append(line);
    		}
    		String bodyString = builder.toString();
    		return objectMapper.readValue(bodyString, Map.class);
    	} catch (Exception e) {
    		e.printStackTrace();
    	} finally {
    		try {
    			reader.close();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    	return new HashMap<>();
    }
    

    补充

    在网上找了一些关于“springboot请求体中流不可重复读取问题”的相关文章,解决了自己的问题,但是觉得整个逻辑不怎么清晰(可能是自己没理解?) 因此,根据自己的思路重新整理了一遍。

    参考文章
  • 相关阅读:
    SVG:中国地图
    网页编程工具:EditPlus
    js插件
    html: 仿制soundmanager2右上角面板
    代码:页面布局(含图片列表布局)
    写着玩: 图片 圆盘
    表格
    按钮
    插件:左侧下拉菜单
    颜色
  • 原文地址:https://www.cnblogs.com/nickhan/p/9849693.html
Copyright © 2020-2023  润新知