大白话讲解如何解决HttpServletRequest的请求参数只能读取一次的问题

大家在开发过程中,可能会遇到对请求参数做下处理的场景,比如读取上送的参数中看调用方上送的系统编号是否是白名单里面的(更多的会用request中获取IP地址判断)、需要对请求方上送的参数进行大小写转换或者字符处理、或者对请求方上送的用户名参数判断是否有对当前请求地址的访问权限(多用于越权处理)等,这些都可以通过实现Filter自定义一个类,在该类中进行相应处理。但是会有个问题,如果是POST请求过来,在Filter的实现类中读取了请求参数,那么后续在Controller里面就无法获取,这是由于我们获取POST请求参数的时候,是通过读取request的IO流来实现的,一旦读取了那么流关闭后,后续就用不了了。
那么解决这个问题,核心考虑就是支持多次读取,可通过缓存方式把流解析的数据缓存下来,这样就可以重复读取缓存下来的数据。举个不恰当的例子,如下

public class MockHttpRequest {
public static String readBook(String bookName) { String fullName = bookName.concat(" 作者:xxx"); System.out.println(fullName); bookName = null; return bookName; } public static void readTwice(String bookName) { bookName = readBook(bookName); System.out.println(bookName); } public static void main(String[] args) { readTwice("hello"); } }
=========执行结果如下=========在readbook方法中读取了bookName后,对bookName做了清空处理(类比流读取后清空),那么第二次访问就拿不到了

hello 作者:xxx
null
如果我们把bookName缓存下来,那么其他方法都读取缓存的字段,就可以实现重复多次读取,如下
public class MockHttpRequest { private static String bufferBook = null; public static String readBook(String bookName) { String fullName = bookName.concat(" 作者:xxx"); System.out.println(fullName); bufferBook = bookName; bookName = null; return bookName; } public static void readTwice(String bookName) { bookName = readBook(bookName); System.out.println(bufferBook); } public static void main(String[] args) { readTwice("hello"); } }

================执行结果如下=============无论readTwice还是更多次,只要读取缓存下来的字段bufferBook,就能一直获取bookName

hello 作者:xxx
hello
所以【解决HttpServletRequest的请求参数只能读取一次的问题】就从数据缓存角度考虑,我们在HttpServletRequest里面创建一个私有属性,用来缓存用户上传的参数,不就可以实现多次读取的功能了吗?我们考虑新生产一个类实现HttpServletRequest,然后在这个类中定义一个属性字段,后续多次读取参数都从这个属性中获取。
tomcat提供的jar中已经有一个应用了装饰器模式的类HttpServletRequestWrapper(该类实现了HttpServletRequest),我们可以定义类NotesHttpServletRequestWrapper extends HttpServletRequestWrapper,然后在NotesHttpServletRequestWrapper中定义一个属性requestBody,具体代码见下:
public class NotesHttpServletRequestWrapper extends HttpServletRequestWrapper {private byte[] requestBody = null; //用来缓存从HttpServletRequest的io流中读取的参数转为字节缓存下来//初始化的时候,就从request读取放到属性字段 //比如在filter中通过NotesHttpServletRequestWrapper requestWrapper = new NotesHttpServletRequestWrapper(request); public NotesHttpServletRequestWrapper(HttpServletRequest request) { super(request); try { requestBody = StreamUtils.copyToByteArray(request.getInputStream()); } catch (IOException e) { e.printStackTrace(); } } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(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 listener) { return; } }; } //获取request请求body中参数 public static Map getRequestParamsFromStream(InputStream in) { String param= null; BufferedReader buffReader=null; try { buffReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8.name())); StringBuilder paramsBuilder = new StringBuilder(); String inputStr; while ((inputStr = buffReader.readLine()) != null) paramsBuilder.append(inputStr); if(!JSONValidator.from(paramsBuilder.toString()).validate()){ return new HashMap(); } JSONObject jsonObject = JSONObject.parseObject(paramsBuilder.toString()); if(jsonObject==null){ return new HashMap(); } param= jsonObject.toJSONString(); } catch (Exception e) { e.printStackTrace(); }finally{ if(buffReader!=null){ try { buffReader.close(); } catch (IOException e) { e.printStackTrace(); } }} return JSONObject.parseObject(param,Map.class); }}

自己实现的HttpServletRequestWrapper类的使用,一般是结合Filter进行,如下:
@Component("notesRequestWrapperFilter") public class NotesRequestWrapperFilter implements Filter {@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) { throw new ServletException("Unsupport request"); }//这里可以做一些请求权限的校验,杜绝越权漏洞 //TODO 比如header中存放token,token解析出账号,判断账号的角色下是否有关联该请求接口HttpServletRequest request = (HttpServletRequest) servletRequest; System.out.println("uri===" + request.getRequestURI()); System.out.println("url===" + request.getRequestURL()); NotesHttpServletRequestWrapper requestWrapper = new NotesHttpServletRequestWrapper(request); //初始化HttpServletRequest的封装类 System.out.println("===HttpMethod.POST.name()===" + HttpMethod.POST.name()); String systemCode = ""; if(HttpMethod.POST.name().equals(request.getMethod())) { //获取request请求body中参数 Map paramsMap = requestWrapper.getRequestParamsFromStream(requestWrapper.getInputStream()); systemCode = paramsMap.get("systemCode"); }else { System.out.println("===请求方式===" + request.getMethod()); systemCode = request.getParameter("systemCode"); }//判断请求方是否位于白名单,系统白名单存在库表、配置中心都可以 List systemsPermit = new ArrayList(); if(!systemsPermit.contains(systemCode)) { throw new ServletException("Unsupport System"); }chain.doFilter(requestWrapper, servletResponse); //注意这个地方往后传的就是我们自己封装的类的实例了 }

【大白话讲解如何解决HttpServletRequest的请求参数只能读取一次的问题】

    推荐阅读