Request的包装类HttpServletRequestWrapper的使用说明

目录
  • Request的包装类HttpServletRequestWrapper使用
    • 大致的意思是:
    • 上述方案解决了
  • HttpServletRequestWrapper和HttpServletResponseWrapper使用时的坑
    • WrapperRequest和WrapperResponse的使用
    • 这里涉及到的坑
      • 坑1
      • 坑2
    • 解决问题
    • 问题延伸

Request的包装类HttpServletRequestWrapper使用

在使用zuul进行鉴权的时候,我们希望从请求Request中获取输入流,解析里面的内容,奈何InputStream只能被读取一次。为啥呢?源码里是这样说的:

public int read(byte[] b,int off, int len)

   Reads up to len bytes of data into an array of bytes from this input stream. Ifpos equals count, then -1 is returned to indicate end of file. Otherwise, the number k of bytes read is equal to the smaller of len and count-pos.If k is positive, then bytes buf[pos] through buf[pos+k-1] are copied into b[off] through b[off+k-1] in the manner performed by System.arraycopy. The value k is added into pos and k is returned.  

大致的意思是:

在InputStream读取的时候,会有一个pos指针,它指示每次读取之后下一次要读取的起始位置。在每次读取后会更新pos的值,当你下次再来读取的时候是从pos的位置开始的,而不是从头开始,所以第二次获取String中的值的时候是不全的,API中提供了一个解决办法:reset()。但我发现在inputStream和servlet中根本不起作用。提示 mark/reset not supported 。意思是只有重写过markSupported()方法的IO流才可以用。所以一般我们使用inputStream,最好在一次内处理完所有逻辑。

那么就没法在中途获取请求流中的数据么?当然有办法了,我可是PPZ,只需要重写Request缓存一下流中的数据就好了,实现代码如下:

BodyReaderHttpServletRequestWrapper.java

package com.neo.authUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class BodyReaderHttpServletRequestWrapper extends
        HttpServletRequestWrapper {
   // private final byte[] body;
     -----》private byte[] body;《-------
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        System.out.println("-------------------打印请求的头信息------------------------------");
        Enumeration<?> e = request.getHeaderNames()   ;
         while(e.hasMoreElements()){
             String name = (String) e.nextElement();
             String value = request.getHeader(name);
            // System.out.println(name+" = "+value);
         }
         -----》获取流中的数据缓存到字节数组中,以后要读数据就用这里的《------
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
    }
    /**
     * 从请求的头部获取用户的身份识别id;
     * @param request
     * @return
     */
    public String getJsessionidFromHeader(HttpServletRequest request) {
        String jsessionid = null;//识别用户身份的id;
        Enumeration<?> e = request.getHeaderNames()   ;
        while(e.hasMoreElements()){
            String name = (String) e.nextElement();
            String value = request.getHeader(name);
            //cookie = JSESSIONID=B926F6024438D4C693A5E5881595160C; SESSION=458e80dc-e354-4af3-a501-74504a873e70
            if("cookie".equals(name)) {
                jsessionid = value.split(";")[0].split("=")[1];
            }
            System.out.println(name+"="+value);
        }
       // System.out.println("======jsessionid========>"+jsessionid);
        return jsessionid;
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
         ------》从缓存的数据中读取数据《------
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            public int read() throws IOException {
                return bais.read();
            }
            @Override
            public boolean isFinished() {
                // TODO Auto-generated method stub
                return false;
            }
            @Override
            public boolean isReady() {
                // TODO Auto-generated method stub
                return false;
            }
            @Override
            public void setReadListener(ReadListener listener) {
                // TODO Auto-generated method stub
            }
        };
    }
    @Override
    public String getHeader(String name) {
        return super.getHeader(name);
    }
    @Override
    public Enumeration<String> getHeaderNames() {
        return super.getHeaderNames();
    }
   /* @Override
    public Enumeration<String> getHeaders(String name) {
        return super.getHeaders(name);
    }  */
    /**
     * content-type=text/plain;charset=UTF-8
     * 重写getHeaders方法,实现自定义Content-Type;
     */
    @Override
    public Enumeration<String> getHeaders(String name) {
        if ((null != name && name.equals("Content-Type"))||(null != name && name.equals("content-type"))) {
            return new Enumeration<String>() {
                private boolean hasGetted = false;
                @Override
                public String nextElement() {
                    if (hasGetted) {
                        throw new NoSuchElementException();
                    } else {
                        hasGetted = true;
                        return "application/json;charset=utf-8";
                    }
                }
                @Override
                public boolean hasMoreElements() {
                    return !hasGetted;
                }
            };
        }
        return super.getHeaders(name);
    }
    /**
     * 添加自定义信息到请求体;
     * @param customMsg:自定义的添加到请求体中的信息;
     */
    public void appendCustomMsgToReqBody(String customMsg) {
        String oldBodyString = HttpHelper.getBodyString(this);//oldBodyString一定是通过当前对象的输入流解析得来的,否则接收时会报EOFException;
        String appendMsg = HttpHelper.appendCustomMsgToReqBody(customMsg);
        String requestBodyAfterAppend = appendMsg + "," +oldBodyString;
        //this.body = HttpHelper.appendCustomMsgToReqBody(HttpHelper.appendCustomMsgToReqBody(customMsg)+(HttpHelper.getBodyString(this))).getBytes(Charset.forName("UTF-8"));
        //this.body = HttpHelper.appendCustomMsgToReqBody((HttpHelper.getBodyString(this))).getBytes(Charset.forName("UTF-8"));
        this.body = HttpHelper.appendCustomMsgToReqBody(requestBodyAfterAppend).getBytes(Charset.forName("UTF-8"));
    }
}

HttpHelper.java

package com.neo.authUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import javax.servlet.ServletRequest;
public class HttpHelper {
    /**
     * 获取post请求中的Body
     *
     * @param request
     * @return
     */
    public static String getBodyString(ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            //读取流并将流写出去,避免数据流中断;
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
    //添加自定义的信息到请求体中;
    public static String appendCustomMsgToReqBody(String newReqBodyStr) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        String newReqBody = null;
        try {
            //通过字符串构造输入流;
            inputStream = String2InputStream(newReqBodyStr);
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //返回字符串;
        newReqBody = sb.toString();
        return newReqBody;
    }
    //将字符串转化为输入流;
    public static InputStream String2InputStream(String str) {
        ByteArrayInputStream stream = null;
        try {
            stream = new ByteArrayInputStream(str.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return stream;
    }
}

上述方案解决了

使用request.getInpuStream()方法读取流中的数据只能读取一次的问题,其实当我们在使用第三方接口时,如果请求头信息和我们的服务所需不一致,例如第三方接口中头部信息为:content-type=text/plain;charset=UTF-8

而我们需要的是:”application/json;charset=utf-8”时,我们也是可以通过重写对应的方法对请求的头部信息进行修改的,代码如下:

/**
     * content-type=text/plain;charset=UTF-8
     * 重写getHeaders方法,实现自定义Content-Type;
     */
    @Override
    public Enumeration<String> getHeaders(String name) {
        if ((null != name && name.equals("Content-Type"))||(null != name && name.equals("content-type"))) {
            return new Enumeration<String>() {
                private boolean hasGetted = false;
                @Override
                public String nextElement() {
                    if (hasGetted) {
                        throw new NoSuchElementException();
                    } else {
                        hasGetted = true;
                        return "application/json;charset=utf-8";
                    }
                }
                @Override
                public boolean hasMoreElements() {
                    return !hasGetted;
                }
            };
        }
        return super.getHeaders(name);
    }

当我们在后端设置了头部信息后,如果不出意外,前端发送的请求将变为简单请求,这样,服务器的处理机制将简单很多。

HttpServletRequestWrapper和HttpServletResponseWrapper使用时的坑

WrapperRequest和WrapperResponse的使用

在做JavaWeb开发过程中如果想拿到请求参数和返回数据的话我们就会使用到这两个类,从类名上就可以看出是包装类,通过这两个类的包装我们可以使用移花接木的方式获取到对应的参数数据。

这里涉及到的坑

坑1

如果请求参数在Body内时取出参数后,后端程序就无法再次取出数据

这个和InputStream不能重复读有关 ,这里需要将Request中的数据自己保存一份然后在使用的时候给出新的InputStream,这样就可以避免使用同一个InputStream读取完数据后无法重新读取数据

@Override
    public ServletInputStream getInputStream() throws IOException {
        //这里每次都重新创建了一个InputStream
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyData);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

坑2

使用HttpServletResponseWrapper包装Response后无法返回数据或者无法返回html,css等数据

这个跟网上的教程有关,大多网上的教程是这样的如下代码:

//先定义一个Filter类包装对应的request和response
public class WrapperFilter extends OncePerRequestFilter {
    private static Logger logger = LoggerFactory.getLogger(WrapperFilter.class);
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            logger.debug(" request mapping {} {}", request.getRequestURL(), request.getRequestURI());
            RequestWrapper requestWrapper = new RequestWrapper(request);
            ResponseWrapper responseWrapper = new ResponseWrapper(response);
            filterChain.doFilter(requestWrapper, responseWrapper);
    }
//response的包装类
public class ResponseWrapper extends HttpServletResponseWrapper {
    private static Logger logger = LoggerFactory.getLogger(ResponseWrapper.class);
    private final ByteArrayOutputStream buffer;
    private final ServletOutputStream out;
    private final PrintWriter writer;

    public ResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        buffer = new ByteArrayOutputStream(2048);
        out = new WrapperOutputStream(buffer);
        writer = new PrintWriter(buffer);
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }
    /**
     *     当获取字符输出流时,实际获取的是我们自己包装的字符输出流
     */
    @Override
    public PrintWriter getWriter() {
        return writer;
    }
   /**
     *     获取返回的数据内容,这个是截获的数据
     */
    public byte[] getResponseData() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }
    public String getContent() throws IOException {
        flushBuffer();
        return buffer.toString();
    }
}

上面的代码虽然是可以获取到数据的但是,数据就无法返回到前端页面了,那么为什么会出现这样的问题呢,咱们来分析一下。

1、包装类对response进行了包装,并且重写了getWriter和getOutputStream 这样就可以保证后端使用response向前端写数据时写到我们定义的buffer中

2、这个包装类是不范围的,也就是只在WrapperFilter 之后有效,但是浏览器从response读取数据明显是在WrapperFilter的范围之外的

也就是说浏览器从reponse读取数据时无法使用ResponseWrapper而只能使用response 这个默认是ResponseFacade

3、那么问题来了咱们上面有往response中写入数据吗,显然是没有的也就是写数据只在ResponseWrapper中有而ResponseFacade 是没有数据的所以浏览器了就无法读取到返回的数据啦。

清楚以上问题后问题就变得简单得多了,那么我们只需要往ResponseFacade 中也定入一份数据就可以了

解决问题

Filter的内容不变

ResponseWrapper中的代码如下修改

public class ResponseWrapper extends HttpServletResponseWrapper {
    private static Logger logger = LoggerFactory.getLogger(ResponseWrapper.class);
    private final ByteArrayOutputStream buffer;
    private final ServletOutputStream out;
    private final PrintWriter writer;

    public ResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        buffer = new ByteArrayOutputStream(2048);
        //这里将response也传入了WrapperOutputStream 和 WrapperWriter
        out = new WrapperOutputStream(buffer,  response);
        writer = new WrapperWriter(buffer, response);
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }
    /**
     *     当获取字符输出流时,实际获取的是我们自己包装的字符输出流
     */
    @Override
    public PrintWriter getWriter() {
        return writer;
    }
    public byte[] getResponseData() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }
    public String getContent() throws IOException {
        flushBuffer();
        return buffer.toString();
    }
}

这里将response也传入了WrapperOutputStream 和 WrapperWriter 这样我们在做数据写入的时候就可以同时向reponse中写入数据了

这两个类的实现如下:

public class WrapperOutputStream extends ServletOutputStream {
    private OutputStream innerOut;
    private HttpServletResponse response;
    public WrapperOutputStream(OutputStream innerOut, HttpServletResponse response) {
        super();
        this.response = response;
        this.innerOut = innerOut;
    }
    @Override
    public boolean isReady() {
        if(response == null){
            return false;
        }
        try {
            return response.getOutputStream().isReady();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    @Override
    public void setWriteListener(WriteListener listener) {
        if(response != null){
            try {
                ((CoyoteOutputStream)response.getOutputStream()).setWriteListener(listener);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //关键在这里
    @Override
    public void write(int b) throws IOException {
        if(response != null){
            response.getOutputStream().write(b);
        }
        innerOut.write(b);
    }
}
//另一个类
public class WrapperWriter extends PrintWriter {
    private HttpServletResponse response;
    ByteArrayOutputStream output;
    public WrapperWriter(ByteArrayOutputStream out, HttpServletResponse response) {
        super(out);
        this.response = response;
        this.output = out;
    }
    //关键在这里
    @Override
    public void write(int b){
        super.write(b);
        try {
            response.getWriter().write(b);
        } catch (IOException e) {
            e.printStackTrace();
            this.setError();
        }
    }
    //关键在这里
    @Override
    public void write(String s, int off, int len) {
        super.write(s,off,len);
        try {
            response.getWriter().write(s,off,len);
        } catch (IOException e) {
            e.printStackTrace();
            this.setError();
        }
    }
}

以上可以看到数据的写入变成了写两份一份写到了自定义的对象中一份写到了response中这样返回到前端的responnse就不会没有数据了

到此问题完全解决,这里还需要注意的就是PrintWriter 有多个writer重载需要都进行重写才行

问题延伸

有人会问能不能直接将response中的OutputStream和Writer获取到分配给对应的WrapperOutputStream 和WrapperWriter而不是直接传入response本身,答案是不可以的,response是不能同时获取OutputStream和Writer的因为他们操作的是同一个数据,所以ResponseFacade 实现时对其进行了保护——同时只能获取OutputStream和Writer中的一个。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Springboot拦截器如何获取@RequestBody参数

    Springboot拦截器获取@RequestBody参数 HttpContextUtils import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader;

  • springboot接口如何多次获取request中的body内容

    1. 概述 在使用springboot开发接口时,会将参数转化为Bean,用来进行参数的自动校验.同时也想获取request中原始body报文进行验签(防止报文传输过程中被篡改). 因为通过将bean再转化为字符串后,body里面的报文格式.字段顺序会发生改变,就会导致验签失败.因此只能通过request来获取body里面的内容. 既想接口自动实现参数校验,同时又想获取request中的原始报文,因此我们可以通过在controller中的restful方法中,写入两个参数,获取多次request

  • 完美解决request请求流只能读取一次的问题

    解决request请求流只能读取一次的问题 实际开发碰到的问题 解决request请求流中的数据二次或多次使用问题 实际开发碰到的问题 springboot项目中,为了防止sql注入,采用Filter拦截器对所有请求流中的json数据进行校验,请求数据没问题则继续向下执行,在后边的代码中应用到请求参数值时,发现request中的json数据为空: 除上边描述的情况,尝试过两次从request中获取json数据,第二次同样是获取不到的. 解决request请求流中的数据二次或多次使用问题 继承Ht

  • Request的包装类HttpServletRequestWrapper的使用说明

    目录 Request的包装类HttpServletRequestWrapper使用 大致的意思是: 上述方案解决了 HttpServletRequestWrapper和HttpServletResponseWrapper使用时的坑 WrapperRequest和WrapperResponse的使用 这里涉及到的坑 坑1 坑2 解决问题 问题延伸 Request的包装类HttpServletRequestWrapper使用 在使用zuul进行鉴权的时候,我们希望从请求Request中获取输入流,解

  • Springboot如何设置过滤器及重复读取request里的body

    目录 HttpServletRequest的输入流只能读取一次的原因 重复读取body中数据的方法 springboot的过滤器 上面的getBody的代码 需求: request的content-type为applciation/json,进入controller之前需要把body中的参数取出来做一次处理,然后和hearder中的另一个参数做对比. 思路: 加一个过滤器,在过滤器中取出参数做处理,然后比较 注意: body里的数据用流来读取,只能读取一次 HttpServletRequest的

  • 详解JSP 内置对象request常见用法

    request 对象是 HttpServletRequestWrapper 类的实例.它的继承体系如下: _request 对象继承层次结构图.png ServletRequest 接口的唯一子接口是 HttpServletRequest ,HttpServletRequest 接口的唯一实现类 HttpServletRequestWrapper ,单从 request 对象一脉单传的类继承体系可以看出,javaweb 标准类库只支持了 http 协议. Servlet/JSP 中大量使用了接口

  • 传智播客java web 过滤器

    根本不利于使用,Servlet应该本是为简化工作而创造的啊!我当时觉得是我的设计框架产生了问题.第二天我便问方老师,确实是使用上有些问题.比如,显示访问计数,我把它单独写成了一个Servlet,什么地方需要它时,便由那个Servlet.include引用计数的Servlet.但这样总会产生一些问题和使用上的不便.比如include的Servlet必须使用相同的流,如果使用forward后任何输出都无效了. 方老师当时建议,把有些功能写到一起.但最后提到了过滤器,那时我便对过滤器产生了兴趣,今日也

  • SpringBoot过滤器如何获取POST请求的JSON参数

    目录 SpringBoot过滤器获取POST请求的JSON参数 想到了使用过滤器来实现这个功能 所以我们可以通过获取到输入流来获取body 从源码我们可以看到 我们创建一个类并继承这个包装类 有一点需要注意的 SpringBoot过滤器获取POST请求的JSON参数 项目中需要将每个请求的路径和请求参数以及响应结果,都记录在日志中,这样在出现问题时可以快速定位是哪里出现了问题. 想到了使用过滤器来实现这个功能 当请求来到过滤器时,会有一个Request参数,通过该参数就能获取到请求路径和请求参数

  • JSP 中文字符处理代码

    网上处理方法一箩筐,下面说说我用过的两种有效地解决办法.1.为程序编写一个字符串处理函数,用一个静态文件保存,在需要处理中文字符的JSP页面中包含它, 复制代码 代码如下: <%! public String codeToString(String str) { String s=str; try { byte temp[]=s.getBytes("ISO-8859-1"); s=new String(temp); return s; } catch(Exception e) {

  • Java中使用COS实现文件上传功能

    cos是O'Rrilly公司开发的一款用于HTTP上传文件的OpenSource组件 需要cos.jar,下载地址:http://www.servlets.com/cos/ cos上传文件很简单,比fileupload还简单:但是上传最大我试了试,是800多兆,超过直接崩溃: java.io.IOException: Posted content length of 1627105576 exceeds limit of 889192448 --->byte,800多M 只需一个servelt即

  • spring boot 自定义参数过滤器,把传入的空字符转换成null方式

    目录 spring boot 滤器,把传入的空字符转换成null 自定义参数处理器 应用启动类 springboot过滤器对请求参数去空格处理 使用 FilterRegistrationBean 注册过滤器 使用@WebFilter注册过滤器 增加JSON字符串的处理 spring boot 滤器,把传入的空字符转换成null 废话不多说直接上代码 自定义参数处理器 public class MyStringArgumentResolver extends AbstractNamedValueM

  • node.js中的http.request方法使用说明

    方法说明: 函数的功能室作为客户端向HTTP服务器发起请求. 语法: 复制代码 代码如下: http.get(options, callback) 由于该方法属于http模块,使用前需要引入http模块(var http= require("http") ) 接收参数: option   数组对象,包含以下参数: host:                  表示请求网站的域名或IP地址(请求的地址). 默认为'localhost'. hostname:        服务器名称,主机

  • node.js中的http.request.end方法使用说明

    方法说明: 完成请求发送. 如果任何一部分请求体没有被发送,它将被刷新到流. 如果指定了 data值,将在执行完 request.end() 后,再执行一条 request.write(data , encoding) 语法: 复制代码 代码如下: request.end([data], [encoding]) 接收参数: data               请求结束后输出的data值 encoding       data值得字符编码

随机推荐