logo头像

From zero to HERO

Java ServletRequest 的一些实战技巧分享

本文于 1487 天之前发表,文中内容可能已经过时。

1. 前言

ServletRequest 是我们搞 Java Web 经常接触的 Servlet Api 。有些时候我们要经常对其进行一些操作。这里列举一些经常的难点操作。

2. 提取 body 中的数据

前后端交互我们会在 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

3. ServletRequest 中的流是一次性的

不要以为上面的读取 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流 只能读取一次 。

4. 可重复读取 ServletRequest 中的流

如果我们使用了个多个 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);
    }
}

5. 如何对 ServletRequest 进行 setParameter()

从前台传入数据的时候、后台通过 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);
      }
  }

你也可借鉴思路实现其它你需要的功能。

6. 总结

今天我们对 ServletRequest 的一些常用的操作进行了讲解。也是我们经常在实际开发中遇到的一些问题。当然你也可以使用一些第三方包来解决这些问题。

评论系统未开启,无法评论!