昨天,我需要做一个从主项目分离出来的项目对主项目的功能的调用,但是在写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());
}
写到这里我对项目接收参数的认识更加清晰了,不知道对各位有没有帮助,如果有什么问题 欢迎联系。