ServletRequest 是我们搞 Java Web 经常接触的 Servlet Api 。有些时候我们要经常对其进行一些操作。这里列举一些经常的难点操作。
前后端交互我们会在 body 中传递数据。我们如何从 body 中提取数据。通常我们会通过 IO 操作:
/** * obtain request body * * @param request the ServletRequest * @return body string it maybe is null */ public static String obtainBody(ServletRequest request) { BufferedReader br = null; StringBuilder sb = new StringBuilder(); try { br = request.getReader(); String str; while ((str = br.readLine()) != null) { sb.append(str); } br.close(); } catch (IOException e) { log.error(" requestBody read error"); } finally { if (null != br) { try { br.close(); } catch (IOException e) { log.error(" close io error"); } } } return sb.toString(); } 复制代码
看起来比较凌乱,各种异常处理,IO 开关操作,很不优雅。 如果你使用了 Java 8 你可以这样简化这种操作:
String body = request.getReader().lines().collect(Collectors.joining()); 复制代码
BufferedReader 提供了获取 Java 8 Stream 流的方法 lines() ,我们可以通过以上方法非常方便的获取 ServletRequest 中的 body
不要以为上面的读取 body 操作是完美无瑕的,这里有一个坑。如果按照上面的操作 ServletRequest 中的 body 只能读取一次。 我们传输的数据都是通过流来传输的。ServletRequest 中我们实际上都是通过:
ServletInputStream inputStream = request.getInputStream() 来获取输入流,然后通过 read 系列方法来读取。Java 中的 InputStream read 方法内部有一个postion, **它的作用是标志当前流读取到的位置,每读取一次,位置就会移动一次,如果读到最后,read 方法会返回 -1,标志已经读取完了,如果想再次读取,可以调用 reset 方法,position 就会移动到上次调用 mark 的位置,mark 默认是 0,所以就能从头再读了。 能否 reset 是有条件的,它取决于 markSupported(),markSupported() 方法返回是否可以进行 mark/reset 。
我们再回头看 ServletInputStream ,其实现并没有重写 reset 方法并不支持 mark/reset 。所以ServletRequest 中的 IO流 只能读取一次 。
如果我们使用了个多个 Servlet Filter 进行链式调用并多次操作 ServletRequest 中的流应该怎么做? 我们可以通过 Servlet Api 提供的 javax.servlet.http.HttpServletRequestWrapper 来对其进行包装。 通过继承 HttpServletRequestWrapper :
public class ReaderRequest extends HttpServletRequestWrapper { private String body;
public ReaderRequest(HttpServletRequest request) throws IOException { super(request); body = request.getReader().lines().collect(Collectors.joining()); } @Override public BufferedReader getReader() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); InputStreamReader inputStreamReader = new InputStreamReader(byteArrayInputStream); return new BufferedReader(inputStreamReader); } 复制代码
} 以下是在一个 Servlet Filter 中的标准范例:
public class TestFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 包装 ReaderRequest cachingRequestWrapper=new ReaderRequest((HttpServletRequest) servletRequest); // 直接从包装读取 String collect = cachingRequestWrapper.getReader().lines().collect(Collectors.joining()); // 传递包装 filterChain.doFilter(cachingRequestWrapper, servletResponse); } }
从前台传入数据的时候、后台通过 HttpServletRequest 中的 getParameter(String name) 方法对数据进行获取。 如果后台想将数据放进去,下次请求或者其他请求时使用,只能通过setAttribute(String name, Object o) 放入然后从 getAttribute(String name) 获取, 无法通过 getParameter(String name) 获取。我在 Spring Security 实战干货: 玩转自定义登录 就遇到了这个问题
首先说一下getParameter(String name) 是在数据从客户端到服务端之后才有效的,而 则是服务端内部的事情,只有在服务端调用了 setAttribute(String name, Object o) 之后,并且没有重定向(redirect),在没有到客户端之前 getAttribute(String name) 才有效。
如果希望在服务端中转过程中使用 setParameter() ,我们可以通过 getParameter(String name) 委托给 getAttribute(String name) 来执行。相关实现依然通过 javax.servlet.http.HttpServletRequestWrapper 来实现。
package cn.felord.spring.security.filter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper;
/** * @author Felordcn * @since 2019/10/17 22:09 */ public class ParameterRequestWrapper extends HttpServletRequestWrapper {
public ParameterRequestWrapper(HttpServletRequest request ) { super(request); } @Override public String getParameter(String name) { return (String) super.getAttribute(name); } 复制代码
} 你也可借鉴思路实现其它你需要的功能。