您的位置:首页 > 其它

Servlet 3.0中的异步处理支持

2017-10-27 14:15 537 查看
来源https://www.javaworld.com/article/2077995/java-concurrency/java-concurrency-asynchronous-processing-support-in-servlet-3-0.html

即使在现代基于UI组件的Web框架和Web服务技术中嵌入了中级API,传入的Servlet 3.0规范(JSR 315)将对Java Web应用程序开发产生突破性的影响。作者Xinyu Liu详细解释了为什么异步处理是定义Web 2.0的协作多用户应用程序的基础。他还总结了Servlet
3.0的其他增强功能,例如易于配置和可插拔性。

Java Servlet规范是大多数服务器端Java Web技术的共同特征,包括JavaServer Pages(JSP),JavaServer Faces(JSF),众多Web框架,SOAP和RESTful Web服务API以及新闻源。运行在这些技术下面的servlet使它们在所有Java
Web服务器(servlet容器)之间都可以移植。对这种广泛接受的用于处理HTTP通信的API的任何建议的更改将潜在地影响所有附属的服务器端Web技术。

即将推出的Servlet 3.0规范于2009年1月通过公开审查,是一个重要的新功能,可以改变Java Web开发人员的生活。以下是您可以在Servlet 3.0中预期的列表:

异步支持

易于配置

可插拔

对现有API的增强

异步支持是Servlet 3.0最重要的增强功能,旨在使Ajax应用程序的服务器端处理更加高效。在本文中,我将重点介绍Servlet 3.0中的异步支持,首先解释支持异步支持的连接和线程消耗问题。然后,我将解释现实世界的应用程序如何在服务器推送实现(如Comet或反向Ajax)中使用异步处理。最后,我将介绍Servlet
3.0的其他增强功能,例如可插拔性和易于配置,让您对Servlet 3.0的良好印象及其对Java Web开发的影响。


异步支持:背景概念

Web 2.0技术大大改变了Web客户端(如浏览器)和Web服务器之间的流量配置。Servlet 3.0中引入的异步支持旨在应对这一新挑战。为了了解异步处理的重要性,我们首先考虑HTTP通信的演进。


HTTP 1.0到HTTP 1.1

HTTP 1.1标准的一个重大改进是持续连接。在HTTP 1.0中,Web客户端和服务器之间的连接在单个请求/响应周期后关闭。在HTTP
1.1中,连接保持活动并重复使用多个请求。持久连接可以明显降低通信滞后,因为客户端在每个请求后不需要重新协商TCP连接。


每个连接线程

了解如何使Web服务器更具可扩展性是供应商面临的一个持续挑战。线程每HTTP连接,这是基于HTTP 1.1的持续连接,是供应商采用的常见解决方案。在此策略下,客户端和服务器之间的每个HTTP连接与服务器端的一个线程相关联。线程从服务器管理的线程池分配。一旦连接关闭,专用线程被回收回到池中,并准备提供其他任务。根据硬件配置,此方法可扩展到大量并发连接。高性能Web服务器的实验产生了数值结果,显示内存消耗几乎与HTTP连接数成正比。原因是线程在内存使用方面相对较贵。配置有固定线程的服务器可能会受到影响线程饥饿问题,一旦所有的线程被采取,新的客户端的请求被拒绝。

另一方面,对于许多网站,用户仅偶尔从服务器请求页面。这被称为逐页模型。大多数时候连接线程空闲,这是浪费资源的。


每个请求的线程

由于Java 4的Java平台(NIO)软件包的新I / O API中引入的非阻塞I / O功能,持久的HTTP连接不需要线程不断附加到它。只有当请求被处理时,才能将线程分配给连接。当连接在请求之间空闲时,线程可以被回收,并且连接被放置在集中式的NIO选择集中以检测新的请求而不消耗单独的线程。这个模型,根据请求调用线程,可能允许Web服务器使用固定数量的线程来处理越来越多的用户连接。使用相同的硬件配置,以此模式运行的Web服务器比线程每连接模式更好。今天,流行的Web服务器(包括Tomcat,Jetty,GlassFish(Grizzly),WebLogic和WebSphere)都通过Java
NIO对每个请求使用线程。对于应用程序开发人员来说,好消息是Web服务器以隐藏的方式实现非阻塞I / O,而不会通过servlet API暴露任何应用程序。


迎接Ajax挑战

通过更灵敏的界面提供更丰富的用户体验,越来越多的Web应用程序使用Ajax。Ajax应用程序的用户与Web服务器的交互频率要高于逐页模型。与普通用户请求不同,Ajax请求可以由一个客户端经常发送到服务器。此外,客户端和客户端上运行的脚本都可以定期轮询Web服务器进行更新。更多同时的请求会导致更多的线程被消耗,这在很大程度上消除了每个请求线程的优点。


运行缓慢,资源有限

一些运行缓慢的后端例程恶化了这种情况。例如,请求可能被耗尽的JDBC连接池或低吞吐量Web服务端点阻止。在资源可用之前,线程可能会长时间停留在待处理的请求中。最好将请求放在等待可用资源的集中式队列中并回收该线程。这有效地节省了请求线程的数量以匹配慢速运行的后端例程的容量。它还表明,在请求处理过程中的某个时刻(当请求存储在队列中时),根本就不会消耗线程。Servlet
3.0中的异步支持旨在通过通用和便携式方法来实现此场景,无论是否使用Ajax。清单1显示了它的工作原理。


清单1.限制对资源的访问

@WebServlet(name="myServlet", urlPatterns={"/slowprocess"}, asyncSupported=true)
public class MyServlet extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response) {
AsyncContext aCtx = request.startAsync(request, response);
ServletContext appScope = request.getServletContext();
((Queue<AsyncContext>)appScope.getAttribute("slowWebServiceJobQueue")).add(aCtx);
}
}

@WebServletContextListener
public class SlowWebService implements ServletContextListener {

public void contextInitialized(ServletContextEvent sce) {
Queue<AsyncContext> jobQueue = new ConcurrentLinkedQueue<AsyncContext>();
sce.getServletContext().setAttribute("slowWebServiceJobQueue", jobQueue);
// pool size matching Web services capacity
Executor executor = Executors.newFixedThreadPool(10);
while(true)
{
if(!jobQueue.isEmpty())
{
final AsyncContext aCtx = jobQueue.poll();
executor.execute(new Runnable(){
public void run() {
ServletRequest request = aCtx.getRequest();
// get parameteres
// invoke a Web service endpoint
// set results
aCtx.forward("/result.jsp");
}
});
}
}
}

public void contextDestroyed(ServletContextEvent sce) {
}
}

[/code]

asyncSupported
属性设置为
true
,方法退出时不提交响应对象。调用
startAsync()
返回
AsyncContext
缓存请求/响应对象对的对象。
AsyncContext
然后将对象存储在应用程序范围的队列中。没有任何延迟,该
doGet()
方法返回,并且原始请求线程被回收。在该
ServletContextListener
对象中,在应用程序启动过程中启动的单独线程监视队列,并在资源可用时继续请求处理。处理请求后,您可以选择调用
ServletResponse.getWriter().print(...)
,然后
complete()
提交响应,或调用
forward()
将流引导到JSP页面以显示结果。请注意,JSP页面是带有的
asyncSupported
属性默认为
false


另外,Servlet 3.0中的类
AsyncEvent
AsynListener
类可以让开发人员精心控制异步生命周期事件。您可以
AsynListener
通过该
ServletRequest.addAsyncListener()
方法注册。在
startAsync()
请求方法被调用之后,一旦异步操作完成或超时,就将a 
AsyncEvent
发送到注册
AsyncListener
。在
AsyncEvent
还包含相同的请求和响应对象作为
AsyncContext
对象。


服务器推送

Servlet 3.0异步功能的一个更有趣和最重要的用例是服务器推送。GTalk,一个允许GMail用户在线聊天的小部件,是服务器推送的一个例子。GTalk不会经常轮询服务器来检查是否有新消息可用于显示。而是等待服务器推回新消息。这种方法有两个明显的优点:低延迟通信,无需发送请求,不浪费服务器资源和网络带宽。

即使同时处理来自同一用户的其他请求,Ajax也允许用户与页面进行交互。常见的用例是让浏览器定期轮询服务器来更新状态更改,而不会中断用户。然而,高轮询频率浪费服务器资源和网络带宽。如果服务器可以主动将数据推送到浏览器
- 换句话说,在事件(状态更改)上向客户端传递异步消息 - Ajax应用程序将执行得更好,并节省宝贵的服务器和网络资源。

HTTP协议是一个请求/响应协议。客户端向服务器发送请求消息,服务器用响应消息进行回复。服务器无法发起与客户端的连接或向客户端发送意外的消息。HTTP协议的这一方面似乎使得服务器推送变得不可能。但是,已经设计了几种巧妙的技术来规避这种限制:

服务流(流)允许服务器在事件发生时向客户端发送消息,而不需要客户端的显式请求。在现实世界的实现中,客户机通过请求发起与服务器的连接,并且每次发生服务器端事件时,响应返回位; 响应(理论上)永远持续。客户端JavaScript可以解释这些位和块,并通过浏览器的增量渲染功能进行显示。

长轮询,也称为异步轮询,是纯服务器推送和客户端拉动的混合。它基于使用基于主题的发布订阅方案的Bayeux协议。在流式传输中,客户端通过发送请求来订阅服务器上的连接通道。服务器保存请求并等待事件发生。一旦事件发生(或者在预定义的超时之后),将向客户端发送完整的响应消息。收到响应后,客户端立即发送新请求。然后,服务器几乎总是有一个出色的请求,它可以用来传送数据以响应服务器端事件。浏览器端的漫游比流式传输更容易实现。

被动捎带:当服务器有更新发送时,它等待下一次浏览器发出请求,然后发送其更新以及浏览器期望的响应。

使用Ajax实现的服务流和长轮询称为Comet或反向Ajax。(一些开发人员将所有的交互技术称为反向Ajax,包括常规投票,彗星和背驮式)。

Ajax提高了单用户的响应能力。像Comet这样的服务器推送技术可以提高协作式,多用户应用程序的应用程序响应能力,而无需常规轮询的开销。

服务器推送技术的客户端方面,例如隐藏
iframe
s,
XMLHttpRequest
流媒体,以及促进异步通信的一些Dojo和jQuery库
- 不在本文的范围之内。相反,我们的兴趣在于服务器端,具体来说,Servlet 3.0规范如何帮助用服务器推送实现交互式应用程序。


高效线程使用:通用解决方案

Comet(流和长轮询)总是占用一个通道,等待服务器推回状态更改。客户端不能重复使用相同的频道发送普通请求。因此,Comet应用程序通常使用每个客户端两个连接。如果应用程序处于线程请求模式,则会将一个未完成的线程与每个Comet连接相关联,从而导致比用户数更多的线程。使用线程每请求方法的Comet应用程序只能以巨大的线程消耗的昂贵的费用来扩展。

Servlet 2.5规范中缺少异步支持,导致服务器厂商通过编写专门的类和接口来设计解决方案,从而将可扩展的Comet编程扩展到Servlet 2.5 API中。例如,Tomcat有一个
CometProcessor
类,Jetty
6有
Continuations
,Grizzly有一个
CometEngine
类。使用Servlet
3.0,在编写可扩展Comet应用程序时,不再需要牺牲便携性。清单1所示的异步功能整合了来自多个供应商的优秀设计思路,并为Comet应用程序中高效的线程使用提供了一个通用的便携式解决方案。

我们来看一个真实世界的例子 - 一个在线拍卖网站。当买家在网上投标一个项目时,它会让他们觉得必须保持刷新页面才能看到最新/最高出价。这是一个麻烦,特别是当招标即将结束时。使用服务器推送技术,不需要页面刷新或高频Ajax轮询。如果出现新的出价,服务器只需将项目的出价提交给注册的客户,如清单2所示。


清单2.拍卖观看

@WebServlet(name="myServlet", urlPatterns={"/auctionservice"}, asyncSupported=true)
public class MyServlet extends HttpServlet {

// track bid prices
public void doGet(HttpServletRequest request, HttpServletResponse response) {
AsyncContext aCtx = request.startAsync(request, response);
// This could be a cluser-wide cache.
ServletContext appScope = request.getServletContext();
Map<String, List<AsyncContext>> aucWatchers = (Map<String, List<AsyncContext>>)appScope.getAttribute("aucWatchers");
List<AsyncContext> watchers = (List<AsyncContext>)aucWatchers.get(request.getParameter("auctionId"));
watchers.add(aCtx); // register a watcher
}

// place a bid
public void doPost(HttpServletRequest request, HttpServletResponse response) {
// run in a transactional context
// save a new bid
AsyncContext aCtx = request.startAsync(request, response);
ServletContext appScope = request.getServletContext();
Queue<Bid> aucBids = (Queue<Bid>)appScope.getAttribute("aucBids");
aucBids.add((Bid)request.getAttribute("bid"));  // a new bid event is placed queued.
}
}

@WebServletContextListener
public class BidPushService implements ServletContextListener{

public void contextInitialized(ServletContextEvent sce) {
Map<String, List<AsyncContext>> aucWatchers = new HashMap<String, List<AsyncContext>>();
sce.getServletContext().setAttribute("aucWatchers", aucWatchers);
// store new bids not published yet
Queue<Bid> aucBids = new ConcurrentLinkedQueue<Bid>();
sce.getServletContext().setAttribute("aucBids", aucBids);

Executor bidExecutor = Executors.newCachedThreadPool();
final Executor watcherExecutor = Executors.newCachedThreadPool();
while(true)
{
if(!aucBids.isEmpty()) // There are unpublished new bid events.
{
final Bid bid = aucBids.poll();
bidExecutor.execute(new Runnable(){
public void run() {
List<AsyncContext> watchers = aucWatchers.get(bid.getAuctionId());
for(final AsyncContext aCtx : watchers)
{
watcherExecutor.execute(new Runnable(){
public void run() {
// publish a new bid event to a watcher
aCtx.getResponse().getWriter().print("A new bid on the item was placed. The current price ..., next bid price is ...");
};
});
}
}
});
}
}
}

public void contextDestroyed(ServletContextEvent sce) {
}
}

[/code]

清单2是非常不言自明的。对于所有的拍卖观察者,未提交的长寿命响应对象都存储在
AsynchContext
缓存在应用程序范围内的对象中
Map
。启动请求/响应循环的原始线程然后返回到线程池,并准备提供其他任务。每当出现新的出价时,在
ServletContextListener
对象中发起的线程从队列中取出,然后将事件推送到为同一拍卖注册的所有观察者(存储的响应对象)。新的出价数据被流传输到连接的浏览器,而不被客户明确要求。这些响应对象未提交用于流式传输,或者为长时间轮询提交。如您所见,在服务器端没有出现任何出价事件时,不会为观察者使用任何线程。

具有Comet功能的新型便携式自定义UI组件(小部件)可以在诸如JSF和Wicket之类的框架下提供。通过Servlet 3.0中的注释和可插拔功能,我将在下一节讨论这些功能 - 这些小部件可以在任何配置下使用。对于应用程序开发人员来说,这肯定是个好消息。


Servlet 3.0中的其他增强功能

除了异步支持之外,Servlet 3.0规范还包括增强功能,使应用程序配置和开发变得更加容易。


易于配置

使用Servlet 3.0,您现在有三个配置Java Web应用程序的选项:注释,API和XML。在Java 5中引入的注释被广泛接受为Java源代码级别的元数据设施。通过注释,对XML配置的需求大大降低。新规范引入了几个方便的注释。the 
@WebServlet
@ServletFilter
and和
@WebServletContextListener
annotations等同于它们在
web.xml
部署描述符文件中的相应标签。


互补技术

适用于各种Java技术的一般规则是,配置集以编程方式优先于XML中的值,并且该XML始终覆盖注释。我认为XML和注释是补充技术。对于与特定Java类,方法,属性密切相关的相对静态的设置,我将使用源代码级的注释。我更喜欢使用XML来实现更加易变的全局设置,而不是与Java源代码绑定,以避免重新编译更改,或者当我无法访问Java源代码(因为类被包装在JAR文件中)时,或者当我需要覆盖注释中定义的设置。

与此同时,该规范增强了
ServletContext
与一些方法的类- 
ServletContext.addServletMapping(...)
ServletContext.addFilter(...)
,例如-允许您以编程方式更改配置。注释和这些新方法作为配置选项的可用性意味着
web.xml
在Java
Web存档(WAR)文件中不再需要一个文件。


可插拔

无论您现在用于Web开发的框架如何,您总是需要在Web应用程序的
web.xml
文件中添加具有特定设置的几个技术强制的servlet或过滤器。更糟的是,如果您想要集成提供自定义GUI小部件或安全功能(如Spring
Security)的技术扩展包,则必须添加更多的servlet,过滤器和参数。您的
web.xml
文件不断增长,并逐渐成为维护麻烦。

虽然Servlet 3.0中的注释使
web.xml
文件成为可选的,但有时候对于诸如增量升级的情况,XML仍然是可取的,以覆盖默认属性值或通过注释设置的值。为了提高框架和组件库的可插拔性,Servlet
3.0引入了一个新的XML 
<web-fragment>
标签,用于指定
web.xml
可以包含在
META-INF
库或框架的JAR文件目录中的Web片段(描述为部分文件的组件列表)。当JAR文件加载并扫描作为整个Web应用程序的一部分时,Servlet容器将拾取并合并片段声明。


对现有API的增强

Servlet 3.0规范对servlet API进行了一些调整。例如,一个
getServletContext
方法被添加到
ServletRequest
类中作为方便方法。新版本的
Cookie
课程现在支持
HTTPOnly
不暴露于客户端脚本代码的Cookie,以减轻跨站点脚本攻击。

在Servlet 3.0规范的早期草案中提出了一些用于编程式登录/注销的方法作为安全增强功能。还值得一提的是,JSR专家组试图使新的servlet API在早期草案中更像POJO,但最终放弃了。毕竟,由于API对应用程序开发人员的影响最小,所以使POJO类似的中低级API不重要。


结论是

Servlet 3.0是一个主要的规范版本。最终版本将包含在Java EE 6中,该EE EE可能在2009年春季首次亮相。Servlet 3.0中的API增强功能,基于注释的配置和可插拔功能必将使Web应用程序开发变得更加容易。最重要的是,长期以来的异步功能统一了API,用于实现诸如Comet之类的服务器推送技术以及资源调节。构建在Servlet
3.0规范之上的Comet应用将比以往更加便携和可扩展。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: