Java Web 流处理笔记

不同ContentType的请求流读取方式

读取请求流一般就用这三种方式:

  1. request.getInputStream()
  2. request.getReader()
  3. request.getParameterNames()/request.getParameterMap()/request.getParameter(key)

记住,流只能读取一次。

其中,request.getInputStream()request.getReader()适用:

  • GET application/x-www-form-urlencoded
  • POST application/json

不适用:

  • GET URI参数
  • POST application/x-www-form-urlencoded

request.getParameter的适用场景刚好相反。

示例代码:
request.getInputStream()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/is")
@ResponseBody
public void is(HttpServletRequest request, HttpServletResponse response) {
String result = "";
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BufferedInputStream in = new BufferedInputStream(request.getInputStream());
int len = -1;
byte[] bytes = new byte[1024 * 5];
while ((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
result = out.toString();
System.out.println(result); // 打印请求内容

} catch (Exception e) {
e.printStackTrace();
}
if(StringUtils.isEmpty(result)){ // 读不到就返回500
response.setStatus(500);
return;
}
response.setStatus(200);
}

request.getReader()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/reader")
@ResponseBody
public void reader(HttpServletRequest request, HttpServletResponse response) {
String result = "";
try {
BufferedReader reader = request.getReader();
StringWriter writer = new StringWriter();
char[] chars = new char[256];
int count = 0;
while ((count = reader.read(chars)) > 0) {
writer.write(chars, 0, count);
}
result = writer.toString();
System.out.println(result); // 打印请求内容

} catch (Exception e) {
e.printStackTrace();
}
if(StringUtils.isEmpty(result)){ // 读不到就返回500
response.setStatus(500);
return;
}
response.setStatus(200);
}

request.getParameterMap()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/param")
@ResponseBody
public void param(HttpServletRequest request, HttpServletResponse response) {
String result = "";
try {
Map<String, String[]> parameterMap = request.getParameterMap();
Set<Map.Entry<String, String[]>> entrySet = parameterMap.entrySet();

for (Map.Entry<String, String[]> entry : entrySet) {
String key = entry.getKey();
String value = Arrays.toString(entry.getValue());
result += key + "=" + value + "&";
}
System.out.println(result); // 打印请求内容

} catch (Exception e) {
e.printStackTrace();
}
if(StringUtils.isEmpty(result)){ // 读不到就返回500
response.setStatus(500);
return;
}
response.setStatus(200);
}

包装请求流

我们经常要在Filter或Interceptor中提前处理请求,如果在这里读了请求流,会导致后面的Controller读不到任何请求参数。为了解决“流只能读取一次”的问题,我们需要一个HttpServletRequestWrapper
HttpServletRequestWrapper示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {

private byte[] body;
private Map<String, String[]> parameterMap = new HashMap<String, String[]>();

private String bodyString;

public MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
// 读请求流
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BufferedInputStream in = new BufferedInputStream(request.getInputStream());
int len = -1;
byte[] bytes = new byte[1024 * 5];
while ((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
body = out.toByteArray(); // 把请求体保存在body
bodyString = new String(body, StandardCharsets.UTF_8);

// 用getInputStream没读到请求体,改用getParameterMap读
if(body.length == 0) {
parameterMap.putAll(request.getParameterMap());
}

} catch (IOException e) {
e.printStackTrace();
}

}

@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {
}
};
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

@Override
public String getParameter(String name) {
String[] values = this.parameterMap.get(name);
if(values != null & values.length>0) {
return values[0];
}
return null;
}

@Override
public Map<String, String[]> getParameterMap() {
return this.parameterMap;
}

@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<String>(parameterMap.keySet());
return vector.elements();
}


public String getBodyString() {
return bodyString;
}

public void setBodyString(String bodyString) {
this.bodyString = bodyString;
}
}

使用了上面这个包装类的Filter示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
@WebFilter(urlPatterns = {"/*"}) // 拦截URI
public class TestFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

MyHttpServletRequestWrapper requestWrapper = new MyHttpServletRequestWrapper(httpServletRequest);

System.out.println("请求URI: " + httpServletRequest.getRequestURI());
System.out.println("请求参数: " + requestWrapper.getBodyString());

// 把原生的httpServletRequest换成requestWrapper传给Controller
chain.doFilter(requestWrapper, httpServletResponse);
}

@Override
public void destroy() {
// TODO Auto-generated method stub
}

}

Zuul网关包装请求流

Spring Cloud的组件Zuul网关的一大功能就是可以由我们自定义一串Filter链,在这些Filter中也会需要读请求流。为了在每个Filter以及网关后面的服务能重复读取请求流,是否要在Zuul网关实现一个HttpServletRequestWrapper?Zuul已经帮我们实现好了。

到达网关的请求一定会经过Servlet30WrapperFilter,在这个Filter中,会把请求封装到Servlet30RequestWrapper,它是HttpServletRequestWrapper的实现类。

Servlet30WrapperFilter源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Servlet30WrapperFilter extends ZuulFilter {

private Field requestField = null;

public Servlet30WrapperFilter() {
this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class,
"req", HttpServletRequest.class);
Assert.notNull(this.requestField,
"HttpServletRequestWrapper.req field not found");
this.requestField.setAccessible(true);
}

protected Field getRequestField() {
return this.requestField;
}

@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public int filterOrder() {
return SERVLET_30_WRAPPER_FILTER_ORDER; // -2,值越小优先级越高
}

@Override
public boolean shouldFilter() {
return true; // TODO: only if in servlet 3.0 env
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request instanceof HttpServletRequestWrapper) {
request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
request);
ctx.setRequest(new Servlet30RequestWrapper(request)); // 用wrapper封装
}
else if (RequestUtils.isDispatcherServletRequest()) {
// If it's going through the dispatcher we need to buffer the body
ctx.setRequest(new Servlet30RequestWrapper(request)); // 用wrapper封装
}
return null;
}

}

当我们在自定义Filter中,用下面两行代码获取HTTP请求时,请求的类型就是Servlet30RequestWrapper

1
2
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest(); // Servlet30RequestWrapper类型

上传文件接口的请求流

WEB项目中经常要实现文件上传,这就涉及到文件流的处理。下面一段代码展示了如何接受文件并保存在本地。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@RequestMapping("/upload")
public String upload(MultipartFile file, HttpServletRequest request) {
// 除了文件以外的请求字段
String name = request.getParameter("name");
String type = request.getParameter("type");
System.out.println("name=" + name);
System.out.println("type=" + type);

// 文件保存到本地
OutputStream os = null;
InputStream is = null;
String fileName = null;
try {
is = file.getInputStream();
fileName = file.getOriginalFilename();
String path = "D:\\test\\";
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流保存到本地文件
File tempFile = new File(path);
if (!tempFile.exists()) {
tempFile.mkdirs();
}
os = new FileOutputStream(tempFile.getPath() + File.separator + fileName);
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
return "fail";
} finally {
// 完毕,关闭所有链接
try {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
} catch (IOException e) {
}
}

return "success";
}

用Postman测试该接口时,Body中Content-Type选择form-data

复制InputStream

如何把一个inputStream复制成多份?需要借助ByteArrayInputStream/OutputStream
示例:

1
2
3
4
5
6
7
8
9
10
11
12
InputStream input =  httpconn.getInputStream();

ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) > -1 ) {
baos.write(buffer, 0, len);
}
baos.flush();
// 复制
InputStream stream1 = new ByteArrayInputStream(baos.toByteArray());
InputStream stream2 = new ByteArrayInputStream(baos.toByteArray());

附:流基本方法

输入流读取数据,输出流写入数据。所以下面的read方法都是InputStream的,write方法都是OutputStream的。

一次读取/写入一个字节

1
2
int read()
void write(int b)

读取方法返回值int,代表从input读到的一个字节内容,可以按ASCII码转为char类型。写入方法的参数int则是要写入output的一个字节。尽管int类型数据最多可以是4个字节,但在流这里就被限制在了0~255之间。循环调用这两个读写方法可以达到读取/写入多个字节的效果,但效率低下,不建议使用

一次写入多个字节

1
2
void write(byte[] data)
void write(byte[] data, int offset, int length)

第一个方法是把字节数组byte[] data的内容全部写入output,第二个方法是指定字节数组从offset开始连续length长度的子数组的内容写入output。数据流向:byte[] -> output
PS:如果使用BufferedOutputStream,往流中写数据时,数据会先存放在一个缓冲区,当缓冲区满了,数据才会被发送出去。最好调用output.flush()强制缓冲区发送数据。

一次读取多个字节

1
2
int read(byte[] data)
int read(byte[] data, int offset, int length)

第一个方法是读取input填充到字节数组byte[] data,第二个方法是读取input填充到字节数组byte[] dataoffset开始连续length长度的子数组。数据流向:input -> byte[]

返回值int代表实际读取的字节数。如下面方法:

1
2
byte[] bytes = new byte[1024];
int len = input.read(bytes);

上面两行代码尝试从输入流input中读取1024字节,但可能一次读取只读到512字节,因此len=512。
如果希望读取输入流input的全部内容,需要多次调用read方法。例如:

1
2
3
4
5
int len = -1;
byte[] bytes = new byte[1024 * 5];
while ((len = input.read(bytes)) != -1) {
output.write(bytes, 0, len);
}

读不到内容时,len=-1,退出循环,此时输入流in的内容全部读完了。
上面代码表示,从输入流input中读取数据,可能一次全部读完,也可能只读出部分,读出的数据填充到bytes,再把bytes的数据写入输出流output,这个过程循环,直到input数据读完。数据流向:input -> byte[] -> output