12.2 身份验证方法

现在你应该知道如何对资源集合实施安全约束,你也应该学会如何验证访问资源的用户信息。由于以声明的方式获得的资源,在部署描述符中使用的是安全约束元素,因此身份验证可以使用HTTP 1.1提供的解决方案:基本访问认证和摘要访问身份验证。此外,还可以使用基于表单的访问认证HTTP身份验证是在RFC2617中定义的。你可以在这里下载规范:
http://www.ietf.org/rfc/rfc2617.txt

基本访问身份验证

基本访问身份验证,或简称基本认证,是一个接受用户名和密码的HTTP身份验证。访问受保护的资源的用户将被服务器拒绝,服务器会返回一个401(未经授权)响应。该响应包含一个WWW-Authenticate头,包含至少一个适用于所请求资源的认证域。这里有一个响应内容的例子:

1
2
3
4
HTTP/1.1 401 Authorization Required
Server: Apache-Coyote/1.1
Date: Wed, 21 Dec 2011 11:32:09 GMT
WWW-Authenticate: Basic realm="Members Only"

浏览器会显示用户输入用户名和密码的登录对话框。当用户单击“登录”按钮时,用户名将被加上一个冒号并与密码连接起来形成一个字符串。该字符串在被发送到服务器之前将用Base64算法编码。成功登录后,服务器将发送所请求的资源。Base64是一个非常弱的算法,因此很容易解密Base64的信息。考虑使用摘要访问认证来替代。

实例

app12b应用程序展示了如何使用基本访问认证

项目结构

app12b应用程序的项目结构如下图所示
这里有一张图片

app12b的部署描述符web.xml

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
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 限制资源访问配置 -->
<security-constraint>
<!-- 访问受限的资源集合 -->
<web-resource-collection>
<web-resource-name>JSP pages</web-resource-name>
<!-- 访问受限的资源集合的URL的正则表达式 -->
<url-pattern>*.jsp</url-pattern>
</web-resource-collection>
<!-- 运行访问该资源集合的角色列表 -->
<auth-constraint />
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Servlet1</web-resource-name>
<!-- 受保护的资源集合的URL正则表达式 -->
<url-pattern>/servlet1</url-pattern>
</web-resource-collection>
<!-- 允许访问该资源集合的角色列表 -->
<auth-constraint>
<role-name>member</role-name>
<role-name>manager</role-name>
</auth-constraint>
</security-constraint>
<!-- 认证设置 -->
<login-config>
<!-- 认证方法为:基本访问身份验证 -->
<auth-method>BASIC</auth-method>
<!-- 提示信息,将会显示在浏览器上 -->
<realm-name>Members Only</realm-name>
</login-config>
</web-app>

部署描述符web.xml中最重要的元素是login-config元素。它有两个子元素:auth-methodrealm-name。要使用
基本访问身份验证 Basic access authentication,您必须将auth-method元素的值设为BASIC(所有字母大写)。在浏览器登录对话框中显示的realm-name元素必须赋值。

Servlet1.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
26
27
28
29
30
31
package servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
urlPatterns =
{"/servlet1"}
)
@ServletSecurity(
@HttpConstraint(
rolesAllowed = "manager"
)
)
public class Servlet1 extends HttpServlet
{
private static final long serialVersionUID = 119L;
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException,IOException
{
RequestDispatcher dispatcher = request
.getRequestDispatcher("/jsp/1.jsp");
dispatcher.forward(request, response);
}
}

1.jsp

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>Hello World</h2>
</body>
</html>

运行效果

要测试app12b例子的基本访问身份验证,可使用以下URL来访问例子中的受限资源。
http://localhost:8080/app12b/servlet1
此时,浏览器中不会显示Servlet1的输出,而是要求输入用户名(tom)和密码(tom),如下图所示:
这里有一张图片
用户名和密码需要在Tomcat安装目录下的conf目录下的tomcat-users.xml中配置,详情请看:12.1 身份验证和授权小节中.
现在输入tomcat-user.xml中配置过的用户名tom,密码tom,就可以访问这个Servlet1了,显示效果如下图所示:
这里有一张图片
认证过后,再次点击上述链接则可以直接访问Servlet1,不再需要重复认证.

摘要访问接入认证

摘要访问接入认证,或简称摘要认证,也是一个HTTP认证,类似基本认证。但不使用弱加密的base64算法,而使用MD5算法创建一个组合用户名、域名和密码的哈希值,并发送到服务器。摘要访问身份验证是为了取代基本的访问认证,因为它提供了更安全的环境。
servletJSP容器没有义务支持摘要访问认证但大多数都有支持。

配置应用程序使用摘要访问认证的方式类似于使用基本访问认证。唯一的区别是login-config元素内的auth-method元素的值。**对于摘要访问认证,login-config元素内的auth-method元素值必须是DIGEST**(大写)。

实例

作为一个例子,该app12c演示应用的摘要访问认证Digest accessauthentication的使用。

项目结构

项目结构如下图所示:

web.xml

此应用程序的部署描述符如下所示:

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
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 设置受保护的资源 -->
<security-constraint>
<!-- 设置受保护的资源集合 -->
<web-resource-collection>
<web-resource-name>JSP pages</web-resource-name>
<!-- URL满足以下正则表达式的资源将被保护 -->
<url-pattern>*.jsp</url-pattern>
</web-resource-collection>
<!-- 设置允许访问该资源集合的角色列表 -->
<!-- 角色列表为空表示拒绝任何角色直接访问 -->
<auth-constraint />
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Servlet1</web-resource-name>
<url-pattern>/servlet1</url-pattern>
</web-resource-collection>
<auth-constraint>
<!-- 允许以下角色访问该资源集合 -->
<role-name>member</role-name>
<role-name>manager</role-name>
</auth-constraint>
</security-constraint>
<!-- 认证配置 -->
<login-config>
<!-- 使用摘要接入认证方法 -->
<auth-method>DIGEST</auth-method>
<!-- 提示信息 -->
<realm-name>Digest authentication</realm-name>
</login-config>
</web-app>

Servlet1.java

与上述代码一样,这里不再列举。

1.jsp

与上面的1.jsp代码一样,这里不再列举.

运行效果

我们在浏览器里输入这个地址来测试一下:
http://localhost:8080/app12c/servlet1
输入正确的用户名tom,密码tom:
这里有一张图片
然后才可以访问Servlet1,浏览器显示效果如下:
这里有一张图片

12.1.2 实施安全约束

WEB-INF目录下的资源客户端不能直接通过URL访问,不过,我们可以通过ServletJSP页面访问WEB-INF目录下的资源。

但是,如果你只是简单地想保护资源拒绝未经授权的用户访问,允许经过授权的用户访问,这可以在部署描述符中声明安全约束来实现。

security-constraint元素说明

部署描述符中的security-constraint元素允许不通过编程就可以限制对某个资源的访问。这个元素有两个子元素:web-resource-collectionauth-constraint

web-resource-collection子元素

web-resource-collection元素表示需要限制访问的资源集合。包括web-resource-namedescriptionurl-patternhttp-methodhttp-method-ommission等子元素。

  • web-resource-name子元素用于设置与受保护资源相关联的名称。
  • url-pattern子元素设置受保护资源的URL的正则表达式,例如:
    • *.jsp表示保护所有的jsp文件
    • /*表示保护所有的资源
    • /jsp/*表示保护jsp目录下的所有资源
    • 不能同时指定目录星号.后缀名,下面的URL表达式指定jsp目录下的所有JSP页面是无效的:/JSP/*.jsp
    • url-pattern仅适用于客户端可以直接访问到的资源。特别是,它不适合于通过MVC体系结构利用RequestDispatcher来访问的页面,不适合于利用类似jsp:forward的手段来访问的页面。
  • http-method元素中写入一个HTTP方法,比如GETPOST,表示该HTTP方法可以访问到这些资源集合.
    • 如果没有http-method元素,这表示将禁止所有HTTP 方法访问这些资源。
  • http-method-omission元素中写入一个HTTP方法,表示使用该方法无法访问到这些资源集合。
    • http-method元素和http-method-omission元素不能出现在相同的web-resource-collection元素里

auth-constraint子元素

auth-constraint元素指定那些用户角色可以访问该受保护的资源集合,通常,此元素应该包含一个或多个role-name元素,

  • 如果security-constraint元素下没有auth-constraint子元素,这表明任何身份的用户都可以访问相应的资源。
  • 如果security-constraint元素下有auth-constraint子元素,但是其内容为空,这表示所有身份的用户都被禁止访问相应的资源

实例:禁止访问特定目录下的资源

下面的web.xml文件的**security-constraint元素限制了所有JSP页面的访问由于auth-constraint不包含role-name元素,是一个空元素,则没有任何用户可以访问JSP页面**。

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 该元素用来限制资源的访问 -->
<security-constraint>
<!-- 需要限制访问的资源集合 -->
<web-resource-collection>
<web-resource-name>JSP pages</web-resource-name>
<!-- 需要限制访问的资源的URL的正则表达式 -->
<!-- 限制所有jsp页面的访问 -->
<url-pattern>*.jsp</url-pattern>
</web-resource-collection>
<!-- 可以上述资源的角色列表 -->
<!-- 内容为空表示没有任何角色可以访问 -->
<auth-constraint />
</security-constraint>
</web-app>

1.jsp

该文件位于jsp页面下,在项目中的路径为:/app12a/WebContent/jsp/1.jsp

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>Hello World</h2>
</body>
</html>

运行效果

现在我们在浏览器里输入这个URL来测试下:
http://localhost:8080/app12a/jsp/1.jsp
JSP页面是无法为访问到的,Servlet容器将发送一个HTTP 403错误给浏览器:表示拒绝访问请求的资源。
浏览器显示效果如下图所示:
这里有一张图片

测试auth-constraint元素

注释掉部署描述符web.xml中的auth-constraint元素,然后重启服务器(修改了web.xml需要重启服务器),再次访问1.jsp页面,现在是可以访问到该页面的,显示效果如下图所示:
这里有一张图片

参考资料

https://www.cnblogs.com/hongzai/articles/3296737.html
https://blog.csdn.net/u012045045/article/details/86612561

12.1.1 指定用户和角色

每一个兼容Servlet/JSP容器必须提供一个定义用户和角色的方法。如果你使用Tomcat,可以通过编辑conf目录中的Tomca-user.xml来创建用户和角色。清单12.1中给出了tomcat-users.xml文件的例子。

tomcat-users.xml

tomcat-users.xml文件是一个根元素为tomcat-userxml文档。其子元素是roleuser元素。

  • role元素定义角色,role元素的rolename属性指定角色名。
  • user元素定义用户。user元素具有usernamepasswordroles属性。
    • username属性指定用户名,
    • password属性指定密码,
    • roles属性指定角色或用户属于的角色。

实例

1
2
3
4
5
6
7
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager"/>
<role rolename="member"/>
<user username="tom" password="tom" roles="manager,member"/>
<user username="jerry" password="tom" roles="member"/>
</tomcat-users>

tomcat-users.xml文件声明了两个角色(经理和成员)和两个用户(tomjerry)。用户tom是一个成员和经理的角色,而jerry只属于成员角色。很明显,tomjerry具有接入更多应用的权限。

Tomcat还支持通过数据库表来匹配角色和用户。你可以配置Tomcat使用JDBC来验证用户身份。

配置到Tomcat中

eclipse中的项目浏览器Project Explorer中点击Servers,然后点击tomcat-users.xml,粘贴如下的代码:

1
2
3
4
<role rolename="manager"/>
<role rolename="member"/>
<user username="tom" password="tom" roles="manager,member"/>
<user username="jerry" password="tom" roles="member"/>

到根标签tomcat-users下,如下图所示:
这里有一张图片

12.1 身份验证和授权

认证是检验某人真的是他自称的那个人的过程。在一个Servlet/JSP应用程序中,**身份验证一般通过检查用户名密码是否正确**。
授权是检查该级别的用户是否具备访问权限。它适用于包括多个区域的应用程序,其中用户可以利用这个应用程序的部分模块,但是其他模块就没有权限。
例如,一个在线商店可被划分成的一般部分(用于一般公众浏览和搜索产品)、买家部分(注册用户下订单)和后台管理部分(适用于管理员)。这三者中,后台管理部分需要访问的最高权限。管理员用户不仅需要进行身份认证,他们还需要获得后台管理部分的权限。
访问级别通常被称为角色。在部署一个Servlet/JSP应用程序时可以方便地通过模块分类和配置,使得每个模块只有特定角色才能访问。这是通过在部署中声明安全约束描述符完成的。换句话说,就是声明式安全。在这个范围的另一端,内容限制是通过编程实现检验用户名和密码与数据库中存储的用户名和密码对是否匹配

大多数ServletJSP应用程序的身份验证授权首先要验证用户名和密码与数据库表是否一致。一旦验证成功,可检查另一个授权在同一个表中存储的用户名和密码的表或字段。

声明式安全

声明式安全的优点

**使用声明式安全可让您的编程更简洁,因为``Servlet/JSP容器负责身份验证和授权过程**。此外,Servlet/JSP容器配置数据库来验证你已经在应用程序中使用。最重要的是,使用声明式身份验证的用户名和密码可在被发送到服务器之前由浏览器对其加密后再发送给服务器。

声明式安全的缺点

声明式安全的缺点是,支持数据加密的身份验证方法只能使用一个默认登录对话框不能对界面和操作进行个性化定制。这个原因就足以让人放弃声明式安全。声明性安全的唯一方法是允许使用一个自定义的HTML表单,不幸的是数据传输不加密。

声明式安全的应用场景

Web应用程序的某些部分,如管理模块,是不面向客户的,所以登录表单的外观是没有关联的。在这种情况下,声明式安全仍然被使用

声明式安全有趣的部分当然就是安全约束不编入Servlet了。相反,它们在应用程序部署时声明在部署描述符中。因此,它具有相当大的灵活性来确定用户和角色对访问的应用程序或部分模块的权限。

如何使用声明式安全

要使用声明式安全,首先定义用户和角色。根据您所使用的容器,您可以将用户和角色信息存储在一个文件或数据库表中,然后,您对应用程序中的资源或集合施加约束。

现在,您如何不通过编程来验证用户?你会发现后面的答案在于HTTP而不是Servlet规范。

第12章安全

安全是网络应用程序开发和部署中的一个非常重要的方面。这是特别真实的,因为任何人都可以通过浏览器连接到万维网,从而随意进入到网络应用程序。要确保应用程序安全,可通过声明方式或可编程方式。下面的四个问题是网络安全的基石:认证、授权、保密性和数据完整性。

  • 身份验证是验证网络实体的身份,特别是用户尝试访问应用程序。您通常会通过询问用户来验证用户身份用户名和密码
  • 授权通常是在与访问级别的身份验证的用户认证成功之后进行的。它试图回答这个问题“一个经过验证的用户可以进入一个应用程序的某个区域吗?”
  • 保密性是一个重要的话题,因为,例如信用卡细节或社会安全号码等敏感数据应予以保护。而且,正如你所知道的,数据是一个电脑在到达另一个互联网目的地之前传播的。拦截它在技术上并不困难。因此,当敏感数据在互联网传输时,应进行加密
  • 由于数据包可以很容易地被截获,只要具备一定的知识和使用工具,则很容易篡改它们。幸运的是,确保敏感数据通过安全通道传送,就有可能保持数据的完整性

在本章中,您将了解这些安全方面的问题。本章还会花很大篇幅讨论**SSL,这是用于在因特网中创建安全通道的协议**。

11.5 小结

Servlet 3.0Servlet 3.1自带用于处理异步操作的功能。当你的Servlet/JSP应用程序非常忙碌,需要一个或更多长时间的操作时,该功能是特别有用的。此功能通过将这些操作分配给一个新的线程,从而将请求处理线程释放回池中,准备好服务另一个请求。在这一章中,你学习了怎样编写支持异步处理的Servlet,以及在处理过程中当某些事件发生时获取通知的监听器。

11.4 异步监听器

为支持Servlet和过滤器配合执行异步操作,Servlet3.0还增加了**AsyncListener接口用于接收异步处理过程中发生事件的通知**。AsyncListener接口定义了如下方法,

onStartAsync方法

当某些事件发生时调用:

1
void onStartAsync(AsyncEvent event)

onComplete方法

在异步操作启动完毕后调用该方法。

1
void onComplete(AsyncEvent event)

onError方法

在异步操作失败后调用该方法。

1
void onError(AsyncEvent event)

onTimeout方法

1
void onTimeout(AsyncEvent event)

获取相关的AsyncEvent

在异步操作超时后调用该方法,即当它未能在指定的超时时间内完成时。
所有四种方法可以分别通过它们的getAsyncContextgetSuppliedRequestgetSuppliedResponse方法,从AsyncContextServletRequestServletResponse中获取相关的AsyncEvent

实例

这里有一个例子,MyAsyncListener类实现AsyncListener接口,以便在异步操作事件发生时,它能够得到通知。请注意,和其他网络监听器不同,你不需要通过@WebListener注解来实现。

MyAsyncListener.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
26
27
28
29
30
31
32
33
34
package listener;
import java.io.IOException;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
// 不需要标注@WebListener
public class MyAsyncListener
implements
AsyncListener
{
@Override
public void onComplete(AsyncEvent asyncEvent)
throws IOException
{
System.out.println("onComplete");
}
@Override
public void onError(AsyncEvent asyncEvent)
throws IOException
{
System.out.println("onError");
}
@Override
public void onStartAsync(AsyncEvent asyncEvent)
throws IOException
{
System.out.println("onStartAsync");
}
@Override
public void onTimeout(AsyncEvent asyncEvent)
throws IOException
{
System.out.println("onTimeout");
}
}

由于AsyncListener类不是用@WebListener注解的,因此必须为AsyncContext手动注册一个AsyncListener监听器,用于接收所需要的事件。通过调用addListener方法为AsyncContext注册一个AsyncListener监听器:

1
void addListener(AsyncListener listener)

AsyncListenerServlet.java

AsyncListenerServlet类是一个异步Servlet,它利用监听器MyAsyncListener获取事件通知。

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
package servlet;
import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import listener.MyAsyncListener;
@WebServlet(
name = "AsyncListenerServlet",
urlPatterns ={"/asyncListener"},
asyncSupported = true
)
public class AsyncListenerServlet extends HttpServlet
{
private static final long serialVersionUID = 62738L;
@Override
public void doGet(final HttpServletRequest request,
HttpServletResponse response)
throws ServletException,IOException
{
//1.获取AsyncContext对象
final AsyncContext asyncContext = request.startAsync();
//2.设置超时时间
asyncContext.setTimeout(5000);
//设置监听器
asyncContext.addListener(new MyAsyncListener());
//3.启动一个线程来处理耗时任务
asyncContext.start(new Runnable()
{
@Override
public void run()
{
//模拟耗时任务
try {
Thread.sleep(3000);
} catch (InterruptedException e) {}
String greeting = "Hi from listener";
request.setAttribute("greeting", greeting);
//4.转到显示页面
asyncContext.dispatch("/test.jsp");
}
});
}
}

test.jsp

1
2
3
4
5
6
<!DOCTYPE HTML>
<html>
<body>
${greeting}
</body>
</html>

运行效果

你可以通过如下URL调用Servlet
http://localhost:8080/app11a/asyncListener
等待3秒钟后,浏览器显示效果如下:
这里有一张图片
控制台输出:

1
onComplete

这表明该异步Servlet结束了

模拟超时

现在修改sleep的时间,如下:

1
2
3
4
//模拟耗时任务
try {
Thread.sleep(6000);
} catch (InterruptedException e) {}

6秒钟比超时时间5秒长,该异步Servlet将会超时.
浏览器显示效果:
这里有一张图片
控制台输出如下:

1
2
3
4
onTimeout
onComplete
Exception in thread "http-nio-8080-exec-28"
......

11.3 编写异步Servlet

写一个异步或异步Servlet过滤器比较简单。**当有一个需要相当长的时间完成的任务时,就需要为之创建一个异步的Servlet过滤器**。

编写异步Servlet或过滤器步骤

在异步Servlet过滤器类中需要做如下操作:
(1)调用ServletRequest中的startAsync方法。该startAsync返一个AsyncContext
(2)调用AsyncContextsetTimeout()方法,设置容器等待任务完成的超时时间的毫秒数。此步骤是可选的,但如果你不设置超时,容器的将使用默认的超时时间。如果任务未能在指定的超时时间内完成,将会抛出一个超时异常。
(3)调用asyncContext.start()方法,传递一个Runnable来执行一个长时间运行的任务。
(4)调用Runnableasynccontext.completeasynccontext.dispatch方法来完成任务。

异步Servlet的doGet或doPost模板

这里是一个异步ServletdoGetdoPost方法的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//(1)获取AsyncContext对象
final AsyncContext asyncContext = servletRequest.startAsync();
//(2)设置超时时间
asyncContext.setTimeout( ... );
//(3)调用asyncContext.start方法,传入一个Runable来执行任务
asyncContext.start(new Runnable()
{
@Override
public void run() {
.....
// 上面是一个需要运行长时间的任务
// (4)任务完成,调用下面函数表示任务完成
asyncContext.complete() or asyncContext.dispatch()
}
})

实例1

一个简单的异步调度的Servlet

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
package servlet;
import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
name = "AsyncDispatchServlet",urlPatterns =
{"/asyncDispatch"},asyncSupported = true
)
public class AsyncDispatchServlet extends HttpServlet
{
private static final long serialVersionUID = 222L;
@Override
public void doGet(final HttpServletRequest request,
HttpServletResponse response)
throws ServletException,IOException
{
// 1.获取AsyncContext实例
final AsyncContext asyncContext = request.startAsync();
// 保存主线程的名称
request.setAttribute("mainThread",
Thread.currentThread().getName());
// 2.设置超时时间为5秒钟
asyncContext.setTimeout(5000);
// 3.启动任务
asyncContext.start(new Runnable()
{
@Override
public void run()
{
// 一个长时间的任务
try {
Thread.sleep(3000);
} catch (InterruptedException e) {}
// 保存耗时任务线程的名称
request.setAttribute("workerThread",
Thread.currentThread().getName());
// 4.任务结束了,调用JSP页面展示
asyncContext.dispatch("/threadNames.jsp");
}
});
}
}

这个Servlet支持异步处理,其长期运行的任务就是简单地休眠5秒钟。为了证明这个耗时的任务是在不同的线程中执行的,而不是在主线程中执行的(即执行 ServletdoGet方法),它将主线程的名字和工作线程的ServletRequest分派到一个threadNames.jsp页面。该threadNames.jsp页面显示mainThreadWorkerThread变量。它们应打印不同的线程名字。

threadNames.jsp

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>Asynchronous servlet</title>
</head>
<body>
Main thread: ${mainThread}
<br/>
Worker thread: ${workerThread}
</body>
</html>

注意,你需要在任务结束后调用asyncContextdispatch方法或complete方法,所以它不会等待,直到它超时。
你可以把你的这个URL输入到浏览器来测试servlet
http://localhost:8080/app11a/asyncDispatch

运行效果

下图显示了主线程的名称和工作线程的名称。你在你的浏览器中看到的可能是不同的,但打印出的线程名字会有所不同,证明了工作线程与主线程不同。
这里有一张图片

除了调度到其他资源去完成任务,你也可以调用AsyncContextcomplete方法。此方法通知servlet容器该任务已完成。

实例2

发送最新进度更新的异步servle

Servlet每秒发送一次进度更新,使用户能够监测进展情况。它发送HTML响应和一个简单的JavaScript代码来更新HTML div元素。

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
package servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AsyncCompleteServlet extends HttpServlet
{
private static final long serialVersionUID = 78234L;
@Override
public void doGet(HttpServletRequest request,HttpServletResponse response)
throws ServletException,IOException
{
response.setContentType("text/html");
final PrintWriter writer = response.getWriter();
writer.println("<html><head><title>" + "Async Servlet</title></head>");
writer.println("<body><div id='progress'></div>");
// 1.获取AsyncContext实例
final AsyncContext asyncContext = request.startAsync();
// 2.设置超时时间
asyncContext.setTimeout(60000);
// 3.启动任务
asyncContext.start(new Runnable()
{
@Override
public void run()
{
System.out.println("new thread:"
+ Thread.currentThread());
// 向浏览器发送循环发送JS代码
for(int i = 0;i < 10;i++) {
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = '"
+ (i * 10) + "% complete'");
writer.println("</script>");
writer.flush();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
// 向浏览器发送最后一个JS
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = 'DONE'");
writer.println("</script>");
writer.println("</body></html>");
// 上面的是代码是一个耗时的任务
// 4.任务结束
asyncContext.complete();
}
});
}
}

以下这段代码负责发送进度更新:

1
2
3
4
5
6
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = '"
+ (i * 10) + "% complete'");
writer.println("</script>");
writer.flush();

浏览器将收到此字符串,其中x是10和100之间的数字。

1
2
3
<script>
document.getElementById('progress').innerHTML = 'x% complete'
</script>

因为该更新代码放在for循环中,将发送10次,浏览器也接收到10次javascript代码,进而执行javascript代码10次,也就是更新div标签10次从而呈现动态的效果.

部署描述符web.xml

为了向你展示如何通过在部署描述符中声明来编写一个Servlet异步,上面的AsyncCompleteServlet不用@WebServlet注解。而是使用下面的部署描述符(web.xml文件)来配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
<servlet-name>AsyncComplete</servlet-name>
<servlet-class>servlet.AsyncCompleteServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>AsyncComplete</servlet-name>
<url-pattern>/asyncComplete</url-pattern>
</servlet-mapping>
</web-app>

小结

在部署描述符中把servlet标签的async-supported子标签设置为true就可以支持异步的Servlet.

显示效果

你可以把你的URL输入到浏览器来测试AsyncCompleteServlet
http://localhost:8080/app11a/asyncComplete
部分显示效果如下:
……
……

11.2 编写异步Servlet和过滤器

新的WebServletWebFilter注解中包含了asyncSupport属性。要编写支持异步处理的Servlet或过滤器,需设置asyncSupported属性为true

1
2
3
4
5
6
7
8
@WebServlet(
asyncSupported=true
...
)
@WebFilter(
asyncSupported=true
...
)

此外,也可以在部署文件里面指定这个描述符。例如,下面的 Servlet 配置为支持异步处理:

1
2
3
4
5
<servlet>
<servlet-name>AsyncServlet</servlet-name>
<servlet-class>servlet.MyAsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>

Servlet过滤器要支持异步处理,可以通过调用ServletRequeststartAsync方法来启动一个新线程。这里有两个startAsync的重载方法:

1
2
3
4
5
6
7
//无参方法
AsyncContext startAsync()
throws java.lang.IllegalStateException
//带参方法
AsyncContext startAsync(ServletRequest servletRequest,
ServletResponse servletResponse)
throws java.lang.IllegalStateException

这两个重载方法都返回一个AsyncContext的实例,这个实例提供各种方法并且包含ServletRequest实例和ServletResponse实例。

  • 第一个重载实例比较简单并且使用方便。由此生成的AsyncContext实例将包含原生的ServletRequest实例和ServletResponse实例。
  • 第二个允许您将原来的ServletRequestServletResponse进行重写封装后传给AsyncContext

需要注意的是,你只能传递原生的ServletRequestServletResponse或它们的封装(装饰)到startAsync第二种重载实例。我们已在第10章“修饰RequestsResponses”中讨论过ServletRequestServletResponse的封装(装饰)。
注意,startAsync重复调用将返回相同的AsyncContext实例。若一个Servlet或过滤器调用startAsync时不支持异步处理,将抛出java.lang.illegalstateexception异常。还请注意,**AsyncContextstart方法是非阻塞的,所以其下一行代码仍将执行**,即使还未调度线程启动。

第11章异步处理

Servlet 3.0引入了一个新功能:异步处理。本章将介绍此功能,并提供实例阐述如何使用它。

11.1概述

一台机器的内存有限。该Servlet/JSP容器设计者知道这一点,并提供了一些可配置的设置,以确保容器内可以运行托管机器的方法。例如,在Tomcat 7中,处理传入的请求的最大线程数是 200。如果你有一个多处理器的服务器,那么你就可以安全地提高这个数字,但除此之外,建议使用该默认值。
Servlet过滤器占有请求处理线程直到它完成任务。如果任务需要很长时间才能完成,当用户的并发请求数目超过线程数时,容器可能会发生无可用线程的风险。如果发生这种情况,Tomcat会堆叠在内部服务器套接字多余的请求(其他容器行为可能不同)。如果有更多的请求进来,它们将被拒绝,直到有空闲资源来处理它们。

异步处理的作用

异步处理功能可以节约容器线程。你应该将异步处理功能使用在长时间运行的操作上。
异步处理功能的作用是释放正在等待完成的线程,使该线程能够被另一请求所使用。

异步处理的使用场景

请注意,这个异步支持只适合你有一个长时间运行的任务并且要把运行结果通知给用户的情况。如果你只有一个长时间运行的任务,但用户并不需要知道处理结果,那么你可以提交一个RunnableExecutor(执行器)然后就可以返回了。

例如,如果你需要生成报告,报告生成需要一定的时间,当它生成完毕时,需要通过邮件发送这个报告,这时servlet异步处理功能不是最佳的解决方案。
相反地,如果你需要生成一个报告,并在报告生成完毕时显示给用户,这时异步处理可能就是你所要的解决方案。