对于过滤器中使用getInputStream()、getParameter()接收参数接收不到的一些知识,以及解决方法。

2023-04-07 14:58:59

昨天,我需要做一个从主项目分离出来的项目对主项目的功能的调用,但是在写Http发送Post请求时,遇到了主项目接收不到参数的情况,从而引起了我对项目接收参数的一些探讨。

我们知道,对于spring项目接收参数用的最多的方式应该是request.getParameter(“xx”),这种方式了把,不论在过滤器Interceptor的preHandle()做拦截是获取参数处理,还是controller用各种注解获取参数比如@RequestParam,@RequestParam(这个注解是获取url后面的参数,下面的post的请求形式上是参数是放在URL后面的,所以能够使用该注解获取)等等。

我们主项目中使用的就在过滤器中使用request.getParameter(“xx”),在controller中使用@RequestParam,获取的参数,今天我在子项目中要调用主项目的一个接口时,需要传一些参数,我就按照平时的发送Http请求写了,代码如下(注意,一些涉及到私密的 我给屏蔽了 ):

/**
     * 发送https请求
     * 
     * @param requestUrl 请求地址
     * @param requestMethod 请求方法(get,post)
     * @param outputStr 请求参数
     * @return JSONObject 返回一个json对象
     */
    public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr){
        JSONObject jsonObject = null;
        System.out.println("----请求参数"+outputStr);
        try {
            URL url = new URL(requestUrl);
            if (url.toString().startsWith("https")){//https请求路径
                HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();

                //创建SSLContext对象,并使用我们指定的信任管理器初始化
                TrustManager[] tm = { new MyX509TrustManager() };
                SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
                sslContext.init(null, tm, new java.security.SecureRandom());
                //从上述的SSLContext对象中得到SSLSocketFactory
                SSLSocketFactory ssf = sslContext.getSocketFactory();
                conn.setSSLSocketFactory(ssf);
                conn.setDoInput(true);
                conn.setDoOutput(true);
                conn.setUseCaches(false);
                //设置请求方式
                conn.setRequestMethod(requestMethod);

                //当outputStr不为null的时候,向输出流写数据
                if(outputStr != null){
                    OutputStream outputStream = conn.getOutputStream();
                    outputStream.write(outputStr.getBytes("UTF-8"));
                    outputStream.close();
                }
                HttpsURLConnection httpConn = conn;
                //从输入流获取数据
                InputStream inputStream = null;
                if (httpConn.getResponseCode() >= 400) {//如果报错,将错误信息写入到输入流中
                    inputStream = httpConn.getErrorStream();
                } else {
                    inputStream = httpConn.getInputStream();
                }
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                String str = null;
                StringBuffer buffer = new StringBuffer();
                while((str = bufferedReader.readLine()) != null){
                    buffer.append(str);
                }
                //释放资源
                bufferedReader.close();
                inputStreamReader.close();
                inputStream.close();
                httpConn.disconnect();
                conn.disconnect();
                System.out.println("HTTP请求返回信息:"+buffer.toString());
                jsonObject = JSON.parseObject(buffer.toString());
            }else{//http请求
                HttpURLConnection conn = (HttpURLConnection)url.openConnection();
                conn.setDoInput(true);
                conn.setDoOutput(true);
                conn.setUseCaches(false);
                //设置请求方式
                conn.setRequestMethod(requestMethod);

                //当outputStr不为null的时候,向输出流写数据
                if(outputStr != null){
                    OutputStream outputStream = conn.getOutputStream();
                    outputStream.write(outputStr.getBytes("UTF-8"));
                    outputStream.close();
                }

                HttpURLConnection httpConn = conn;
                //从输入流获取数据
                InputStream inputStream = null;
                if (httpConn.getResponseCode() >= 400) {//如果报错,将错误信息写入到输入流中
                    inputStream = httpConn.getErrorStream();
                } else {
                    inputStream = httpConn.getInputStream();
                }
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                String str = null;
                StringBuffer buffer = new StringBuffer();
                while((str = bufferedReader.readLine()) != null){
                    buffer.append(str);
                }

                //释放资源
                bufferedReader.close();
                inputStreamReader.close();
                inputStream.close();
                httpConn.disconnect();
                conn.disconnect();
                System.out.println("HTTP请求返回信息:" + buffer.toString());
                jsonObject = JSON.parseObject(buffer.toString());
            }

        } catch (ConnectException ce) {
            ce.printStackTrace();
            log.error("连接超时:{}",ce);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("https请求异常:{}", e);
        }
        return jsonObject;
    }

上面的请求参数是一个json字符串数据,用于请求参数,

但是单元测试的时候,这个http请求总是返回说参数不存在的400错误。

从上面的代码可以看出,我明明是把请求参数写入到了输出流当中了。然后我从主项目的过滤器中使用request.getParameter(“xx”),获取 是一个null值。

刚开始我以为是我的数据没有写进来,但是后来我在主项目中使用流读取参数,确实是能够读取到参数的。这就是问题所在,说明我是把请求参数写入进来了。但是获取不到。

所以我就开始寻找相关的信息,后来从别的博客以及资料中,了解到好像request.getParameter(“xx”)这种获取参数的方法,仅仅对于form表单提交的请求有效,并且form表单还需要设置enctype=”application/x-www-form-urlencoded”是编码方式,这个是form的默认编码方式,所以如果不是设置的其他的编码格式就能够获取到。

知道了这个,我就开始在我的http方法中添加了:

conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

设置了请求编码类型,然后再请求,发现一点用没有,还是报参数不存在的错误。但是我从公司的swagger-ui上测试接口是能够测试通的。。

后来我使用Fiddler工具监听了swagger-ui调用接口的请求,发现,接口传输的参数不是在request的body区域内,而是拼接到了URL上面,也就是说虽然这是一个POST请求,但是参数的传输还是在URL上面。然后我就查询各种资料,然后问了公司的前端工程师、安卓工程师,他们调用也没有问题,我看了一下他们的调用代码,,前端工程师他也是先将参数处理成一个URL后面的字符串,不过他不是将这个字符串拼接到URL后面,而是把这个字符串写入到Http的body里面。安卓的就是把这个json写入到body属性中。

然后我就开始修改我的http请求,模拟form表单提交的方式发送Http请求(从这个可以看出来,java模拟form表单提交与普通的http请求的区别,就是下面这个 还有一个请求头的content-type的设置问题),在请求前对参数进行处理:


        // 构建请求参数
        StringBuffer sb = new StringBuffer();
        if (outputStr != null) {
            Map params = JSONObject.parseObject(outputStr);
            for (Object e : params.keySet()) {
                sb.append("&");
                sb.append(e);
                sb.append("=");
                sb.append(params.get(e));
            }
            sb.substring(0, sb.length() - 1);
        }

将原来的请求参数拼接成以下格式的请求参数:
&key1=xxx&key2=xxx&key3=xxx

然后在将拼接好的请求参数写入到request中。单元测试发现主项目中使用request.getParameter能够获取到参数了。

虽然这个问题解决了,但是对于项目接收参数还是有很多疑问,比如说在过滤器中如何使用流接收参数,以及为什么在过滤器或者其他地方或去过参数之后controller里面就再也获取不到参数了。。

首先说第二个问题,为什么在过滤器或者其他地方或去过参数之后controller里面就再也获取不到参数了。。

这个问题主要是一个HttpServletRequest的一个不知道是不是bug的问题,就是对于一个request请求来说,它的参数输入流只能读取一次,读取之后流中的数据便没有了,而无论我们从过滤器中也好还是三方的一些功能里面也好还是controller,只要它需要使用到request中的参数,他就只能从流中读取。所以如果在controller之前,有对象都去过request中的流,那么controller中就再也读取不到参数了。。

那么这种问题如何处理呢,现在使用最多的一种方式便是我们现将流读出来,然后在写进去。这样后面的方法在读取的时候就能够读取了。

这也就是第一个问题过滤器中如何使用流接收参数

我们在过滤器中使用流读取参数,我们需要考虑我们读完之后,后面的是不是也能读到。我们不能做那种我们自己读完了一时爽,然后让后面的人懵逼去吧的事情。。。

下面是解决方法:

既然原生的ServletRequest有这样的问题,那么我们可以自己写一个ServletRequest,能够提供重复对取请求参数流的方法,这个就需要继承HttpServletRequestWrapper方法。

package ***.***.***.***.common;

/**
 * Created by yefuliang on 2017/10/25.
 */

import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * 保存流
 *
 * @author yefuliang 2017年10月25日
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;//保存流的字节数组

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String sessionStream = getBodyString(request);//读取流中的参数
        body = sessionStream.getBytes(Charset.forName("UTF-8"));
    }

    /**
     * 获取请求Body
     *
     * @param request
     * @return
     */
    public String getBodyString(final ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = cloneInputStream(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();
    }

    /**
     * Description: 复制输入流</br>
     *
     * @param inputStream
     * @return</br>
     */
    public InputStream cloneInputStream(ServletInputStream inputStream) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = inputStream.read(buffer)) > -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            byteArrayOutputStream.flush();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        return byteArrayInputStream;
    }
    @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() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

        };
    }
}

上面继承HttpServletRequestWrapper的方法写好了,我们就可以使用了,具体的使用方法如下:
1.首先在拦截器中将原来的ServletRequest替换掉:

// 防止流读取一次后就没有了, 所以需要将流继续写出去
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);

HttpServletResponse resp = (HttpServletResponse) servletResponse;
ResponseWrapper mResp = new ResponseWrapper(resp); // 包装响应对象 resp 并缓存响应数据

filterChain.doFilter(requestWrapper, mResp);

可以对比下以前的doFilter()方法:filterChain.doFilter(request, response);
可以发现我上面的代码对request和response都进行了封装,封装response主要是我这个项目还需要对返回的参数进行处理,因为别的地方都读取不到reponse中的值,所以在这里重新封装了response用与读取返回值,具体的怎么封装可以看我的另一篇博客过滤器通过HttpServletResponseWrapper包装HttpServletResponse实现获取response中的返回数据,以及对数据进行gzip压缩

2.在过滤器中如果需要读取参数:

JSONObject parameterMap = JSON.parseObject(new BodyReaderHttpServletRequestWrapper(request).getBodyString(request));
String dataFrom = String.valueOf(parameterMap.get("dataFrom"));

parameterMap 就是请求的参数json。

3.如何在controller中获取q请求的json数据
可以参考下我下面的方法,使用@RequestBody 将请求参数转换成后面的类型的参数,后面的可以是一个Bean,也可以是一个json,也可以是一个string,看你传输的数据了:

    @RequestMapping("/***/manageUserGag")
    public String manageUserGag(@RequestBody JSONObject request){
        return ***Impl.manageUserGag(request.toJSONString());
    }

写到这里我对项目接收参数的认识更加清晰了,不知道对各位有没有帮助,如果有什么问题 欢迎联系。

  • 作者:落_尘
  • 原文链接:https://blog.csdn.net/qq_33206732/article/details/79271912
    更新时间:2023-04-07 14:58:59