简述

服务端报了一个异常:java.lang.IllegalStateException: STREAMED,详细堆栈如下。

1
2
3
4
5
11:30:51.359 ERROR [qtp463355-1242] [API](GetServlet.java:77)
java.lang.IllegalStateException: STREAMED
at org.eclipse.jetty.server.Request.getReader(Request.java:1188)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)

这个从堆栈上看是jetty的问题,看着是框架的问题,但是还是得分析分析。
看了一个代码,是代码是同时使用了jetty的两个API,原是是不能同时使用下面这两个方法,就是在一次请求里,不能同时使用这两个:

  • request.getReader()
  • request.getParameter()

原是为什么,官方文档也有说明:

If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.

https://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/ServletRequest.html#getParameter(java.lang.String)

具体是什么原因,那处好好分析一下。

源码分析

先说源码层面的原因:流状态被置为已读取,当有其它方法来读取,判断状态已读取,直接抛异常。
HTTP 接口使用 form 表单形式和 json 表单形式的内部处理机制不同导致form只能读一次,而 json 可以反复读取。tomcat 和 jetty 使用了相同的设计。

发看生问题的方法: getInputStream()
先看发生问题的地方,再分析getParameter()getReader() 为什么会报错。

getInputStream 方法

流处理方法: getInputStream(),HTTP 的读取状态会存储在 _input中。是否已经被读取的状态由 _inputState 控制。
如果只有要这个方法被读次一次,那么_inputState = INPUT_STREAM,下面代码中的第8行就行判断是否被读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* @see javax.servlet.ServletRequest#getInputStream()
*/
@Override
public ServletInputStream getInputStream() throws IOException
{
// 如果不是 INPUT_NONE、INPUT_STREAM 状态,流已经被读取过,状态已变更
if (_inputState != INPUT_NONE && _inputState != INPUT_STREAM)
throw new IllegalStateException("READER");
// _inputState 变更为 INPUT_STREAM
_inputState = INPUT_STREAM;

if (_channel.isExpecting100Continue())
_channel.continue100(_input.available());

return _input;
}

Request 类

这个是发生问题的类:org.eclipse.jetty.server.Request,如果状态已被读取: _inputState = INPUT_STREAM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Request implements HttpServletRequest
{
// 未读取状态
private static final int INPUT_NONE = 0;
// 已读取状态!!!
private static final int INPUT_STREAM = 1;
...

public static Request getBaseRequest(ServletRequest request)
{
// 读取状态,默认 INPUT_NONE
private int _inputState = INPUT_NONE;
...
}
}

getParamter() 分析

调用栈

1
2
3
4
5
request.getParameters()
|--getParameters()
|--extractContentParameters()
|--extractFormParameters(MultiMap<String> params)
|--InputStream in = getInputStream(); //在这个位置 getInputStream

getParameter 分析

可以读取到 表单提交的数据、GET 方法参数,但是无法读取 JSON 数据。

所以如果是以 JSON 形式提交的数据,不会被它操作到,也不会去操作 Stream

  • extractContentParameters:从流中读取 POST 传入的数据
    • 判断是content-type否为 application/x-www-form-urlencoded
      • extractFormParameters:解析 form 表单数据,读取,调用 getInputStream 方法
  • extractQueryParameters:从 GET 的URI 中 解析请求参数

form 处理流程

涉及方法:

  • Request.getParameter()
  • Request.getInputStream()

提交 form 表单是处理时,Request.getParameter()会调用 getInputStream(),具体调用栈分析:

Text
1
2
3
4
5
request.getParameters("xxx")
|--getParameters()
|--extractContentParameters()
|--extractFormParameters(_contentParameters) // 注意在这里会调用 getInputStream()
|--getInputStream()

所以这个问题的关键在于 Request.getParameter() 方法,会调用 getInputStream();

1
2
3
4
5
6
7
8
9
10
11
12
13
public void extractFormParameters(MultiMap<String> params)
{
try
{
...
InputStream in = getInputStream();
...
}
catch (IOException e)
{
...
}
}

JSON 处理流程

在读到 Request.getParameter()在读取 JSON 数据时,并不会走读取流的逻辑,因为会判断content-type是否为 application/x-www-form-urlencoded

JSON 的 content-type 为:application/json
所以,提交的是JSON请求时,getParameter()直接返回空。

getReader() 方法

这个方法,比较直观,上来直接就是 getInputStream()读取流。
getReader 只能处理 POST 请求参数,没有处理 URI 的功能。
所以如果 getParameter 提前把流读取了,getReader() 就无法获取到流,直接抛异常。

Text
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
public BufferedReader getReader() throws IOException
{
if (_inputState != INPUT_NONE && _inputState != INPUT_READER)
throw new IllegalStateException("STREAMED");

if (_inputState == INPUT_READER)
return _reader;

String encoding = getCharacterEncoding();
if (encoding == null)
encoding = StringUtil.__ISO_8859_1;

if (_reader == null || !encoding.equalsIgnoreCase(_readerEncoding))
{
// 读取流
final ServletInputStream in = getInputStream();
_readerEncoding = encoding;
_reader = new BufferedReader(new InputStreamReader(in, encoding))
{
@Override
public void close() throws IOException
{
in.close();
}
};
}
_inputState = INPUT_READER;
return _reader;
}

结论

getParameter 方法,可以读到:

  • POST from 表单数据
    • 但是无法读取 JSON 数据,因为从源码中看,它只处理/x-www-form-urlencoded
  • GET 数据
    getReader 方法则是直接读取 JSON 数据。