2.13 Servlet3.1新增的非阻塞式IO

2.13 Servlet3.1新增的非阻塞式IO

最新的Java EE 7已经发布,伴随Java EE 7一起发布了Servlet3.1,Servlet3.1引入了少数新特性。Servlet3.1新特性包括强制更改sessionId(由HttpServletRequestchangeSessionId()方法提供)、非阻塞IO等。尤其是Servlet3.1提供的非阻塞IO进行输入、输出,可以更好地提升性能。
Servlet底层的IO是通过如下两个IO流来支持的。

  • ServletInputStream:Servlet用于读取数据的输入流。
  • ServletOutputStream:Servlet用于输出数据的输出流。

Servlet读取数据为例,传统的读取方式采用阻塞式IO——当Servlet读取浏览器提交的数据时,如果数据暂时不可用,或数据没有读取完成,Servlet当前所在线程将会被阻塞,无法继续向下执行。

ReadListener

Servlet3.1开始,ServletInputStream新增了一个setReadListener(ReadListener readListener)方法,该方法允许以非阻塞IO读取数据,实现ReadListener监听器需要实现如下三个方法

  • onAllDataRead():当所有数据读取完成时激发该方法。
  • onDataAvailable():当有数据可用时激发该方法
  • onError(Throwable t):读取数据出现错误时激发该方法。

类似地,ServletOuputStream也提供了setWriterListenerer(WriteListener writeListener)方法,通过这种方式,可以让ServletOuputStream以非阻塞IO进行输出

Servlet中使用非阻塞IO步骤

Servlet中使用非阻塞IO非常简单,主要按如下步骤进行即可:

  • 调用ServletRequeststartAsync()方法开启异步模式
  • 通过ServletRequest获取ServletInputStream,并为ServletInputStream设置监听器(ReadListener实现类)。
  • 实现ReadListener接口来实现监听器,在该监听器的方法中以非阻塞方式读取数据。

程序示例

项目结构

G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\02\2.13\servlet31
├─async.jsp
├─form.html
└─WEB-INF\
  ├─build.xml
  ├─classes\
  │ └─lee\
  │   ├─AsyncServlet.class
  │   └─MyReadListener.class
  ├─lib\
  ├─src\
  │ └─lee\
  │   ├─AsyncServlet.java
  │   └─MyReadListener.java
  └─web.xml

下面的Servlet负责处理表单页面提交的数据,但该Servlet并未使用传统的、阻塞IO来读取客户端数据,而是采用非阻塞IO进行读取。下面是该Servlet的代码。

AsyncServlet.java

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
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("<title>非阻塞IO示例</title>");
out.println("进入Servlet的时间:" + new java.util.Date() + ".<br/>");
// 创建AsyncContext,开始异步调用
AsyncContext context = request.startAsync();
// 设置异步调用的超时时长
context.setTimeout(60 * 1000);
ServletInputStream input = request.getInputStream();
// 为输入流注册监听器
input.setReadListener(new MyReadListener(input, context));
out.println("结束Servlet的时间:" + new java.util.Date() + ".<br/>");
out.flush();
}
}

上面程序调用requeststartAsync()方法开启异步调用之后,程序中粗体字代码为Servlet输入流注册了一个监听器,这样就无须在该Servlet中使用阻塞IO来获取数据了。而是改为由MyReadListener负责读取数据,这样Servlet就可以继续向下执行,不会因为IO阻塞线程。
MyReadListener需要实现Readlistener接口,并重写它的三个方法。

MyReadListener.java

下面是MyReadListener的代码。

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
package lee;

import javax.servlet.*;
import java.io.*;

public class MyReadListener implements ReadListener {
private ServletInputStream input;
private AsyncContext context;

public MyReadListener(ServletInputStream input, AsyncContext context) {
this.input = input;
this.context = context;
}

@Override
public void onDataAvailable() {
System.out.println("数据可用!!");
try {
// 暂停5秒,模拟读取数据是一个耗时操作。
Thread.sleep(5000);
StringBuilder sb = new StringBuilder();
int len = -1;
byte[] buff = new byte[1024];
// 采用原始IO方式读取浏览器向Servlet提交的数据
while (input.isReady() && (len = input.read(buff)) > 0) {
String data = new String(buff, 0, len);
sb.append(data);
}
System.out.println(sb);
// 将数据设置为request范围的属性
context.getRequest().setAttribute("info", sb.toString());
// 转发到视图页面
context.dispatch("/async.jsp");
} catch (Exception ex) {
ex.printStackTrace();
}
}

@Override
public void onAllDataRead() {
System.out.println("数据读取完成");
}

@Override
public void onError(Throwable t) {
t.printStackTrace();
}
}

上面程序中MyReadListeneronDataAvailable()方法先暂停线程5秒,用于模拟耗时操作,接下来程序使用普通IO流读取浏览器提交的数据。
如果程序直接让Servlet读取浏览器提交的数据,那么该Servlet就需要阻塞5秒,不能继续向下执行;改为使用非阻塞IO进行读取,虽然读取数据的IO操作需要5秒,但它不会阻塞Servlet执行,因此可以提升Servlet的性能。
程序使用一个简单的表单向该Servlet提交请求,该表单内包含了请求数据。提交的表单效果如图2.53所示。