我是靠谱客的博主 跳跃蜜粉,这篇文章主要介绍Servlet异步处理特性分析与实践,现在分享给大家,希望可以做个参考。

  众所周知,Servlet 3.0标准已经发布了很长一段时间,相较于之前的2.5版的标准,新标准增加了很多特性,比如说以注解形式配置Servlet、web.xml片段、异步处理支持、文件上传支持等。虽然说现在的很多Java Web项目并不会直接使用Servlet进行开发,而是通过如Spring MVC、Struts2等框架来实现,不过这些Java Web框架本质上还是基于传统的JSP与Servlet进行设计的,因此Servlet依然是最基础、最重要的标准和组件。在Servlet 3.0标准新增的诸多特性中,异步处理支持是令开发者最为关注的一个特性,本文就将详细对比传统的Servlet与异步Servlet在开发上、使用上、以及最终实现上的差别,分析异步Servlet为何会提升Java Web应用的性能。

  Web容器会为每个请求分配一个线程,默认情况下,响应完成前,该线程占用的资源都不会被释放。若有些请求需要长时间处理(例如长时间运算、等待某个资源),就会长时间占用线程资源,若这类请求过多,许多线程资源都被长时间占用,会对系统的性能造成负担。Servlet 3.0新增了异步处理,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应

1、冗长请求实例

  当今的应用已经不仅仅是被动地等待浏览器来发起请求,而是由应用自身发起通信。典型的示例有聊天应用、拍卖系统等等,实际情况是大多数时间与浏览器的连接都是空闲的,等待着某个事件来触发。这种类型的应用自身存在着一个问题,特别是在高负载的情况下问题会变得更为严重。典型的症状有线程饥饿、影响用户交互等等。根据近一段时间的经验,我认为可以通过一种相对比较简单的方案来解决这个问题。在Servlet API 3.0实现成为主流后,解决方案就变得更加简单、标准化且优雅了。在开始介绍Servlet 3.0的解决方案前,我们先看一个冗长的请求实例,代码如下:

复制代码
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
//假设我们有一个Servlet需要很多的时间来处理, //类似下面的这个LongRunningServlet,完成响应需要很长时间 @WebServlet("/LongRunningServlet") public class LongRunningServlet extends HttpServlet { private static final long serialVersionUID = 1L; public LongRunningServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); System.out.println("LongRunningServlet Start::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId()); //需要在Get请求中添加time变量参数 String time = request.getParameter("time"); if(null == time) time = "1"; int secs = Integer.valueOf(time); if(secs > 10000) secs = 10000; longProcessing(secs); PrintWriter out = response.getWriter(); long endTime = System.currentTimeMillis(); response.setContentType("text/html;charset=UTF-8"); out.println("<!DOCTYPE html PUBLIC " + "'-//W3C//DTD HTML 4.01 Transitional//EN'>"); out.println("<html>"); out.println("<head>"); out.println("<title>LongRunningServlet</title>"); out.println("</head>"); out.println("<body>"); out.write("<h1>"); out.write("Processing done for " + secs + " milliseconds!</h1>"); System.out.println("LongRunningServlet Start::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId() + "::Time taken=" + (endTime - startTime) + " milliseconds!"); out.println("</body>"); out.println("</html>"); out.close(); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } private void longProcessing(int secs){ try { Thread.sleep(secs); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } }

  在浏览器上请求URL:http://localhost:8088/AsyncServletTest/LongRunningServlet?time=3000,得到响应如图所示:

  查看控制台中打印的信息如下:

复制代码
1
2
LongRunningServlet Start::Name=http-nio-8088-exec-2::ID=29 LongRunningServlet Start::Name=http-nio-8088-exec-2::ID=29::Time taken=3001 milliseconds!

  所以Servlet线程实际运行超过3秒。这可能导致线程饥饿——因为我们的Servlet线程被阻塞,直到所有的处理完成。上面的Servlet主要完成以下事情:
  1、请求到达,表示开始监控某些事件。
  2、线程被阻塞,直到事件发生为止。
  3、在接收到事件后,编辑响应然后将其发回给客户端。

  为了简化,代码中将处理耗时请求的部分替换为一个Thread.sleep()调用。如果服务器的得到了很多这样的耗时请求,它将达到最大Servlet线程限制。Servlet 3.0之前,这些长期运行的线程,容器有其特定的解决方案,我们可以产生一个单独的工作线程完成耗时的任务,然后返回响应客户。Servlet线程返回Servlet池后启动工作线程。Tomcat 的 Comet、WebLogic FutureResponseServlet 和 WebSphere Asynchronous Request Dispatcher都是实现异步处理的很好示例。容器特定解决方案的问题在于,在不改变应用程序代码时不能移动到其他Servlet容器。这就是为什么在Servlet3.0提供标准的方式异步处理Servlet的同时增加异步Servlet支持

2、使用AsyncContext进行异步处理

  为了支持异步处理,Servlet 3.0中,在ServltRequest上提供了startAsync()方法:

复制代码
1
2
AsyncContext startAsync() throws java.lang.IllegalStateException; AsyncContext startAsync(ServletRequest servletRequest,ServletResponse servletResponse) throws java.lang.IllegalStateException;

  这两个方法都会返回AsyncContext接口的实现对象,前者会直接利用原有的请求与响应对象来创建AsyncContext,后者可以传入自行创建的请求、响应封装对象。在调用了startAsync()方法取得AsyncContext对象之后,此次请求的响应会被延后,并释放容器分配的线程

  可以通过AsyncContext的getRequest()、getResponse()方法取得 请求、响应对象,此次对客户端的响应将暂缓至调用AsyncContext的complete()或dispatch()方法为止,前者表示响应完成,后者表示将调派指定的URL进行响应。

  若要能调用ServletRequest的startAsync()以取得AsyncContext,必须告知容器此Servlet支持异步处理,如果使用@WebServlet来标注,则可以设置其asyncSupported为true。例如:

复制代码
1
2
3
@WebServlet(urlPatterns = {"/AsyncServlet"}, asyncSupported = true) public class AsyncServlet extends HttpServlet { ...

  如果使用web.xml设置Servlet,则可以在<servlet>中设置<async-supported>标签为true。

复制代码
1
2
3
4
5
<servlet> <servlet-name>AsyncServlet</servlet-name> <servlet-class>com.servlet.AsyncServlet</servlet-class> <async-supported>true</async-supported> </servlet>

  如果Servlet要进行异步处理,若其前端有过滤器,则过滤器亦需标示其支持异步处理,如果使用@WebFilter,同样设置其asyncSupported为true,例如:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebFilter( filterName="PerformanceFilter", asyncSupported=true, urlPatterns={"/*"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC }, initParams = { @WebInitParam(name = "Site", value = "xxxxx") }) public class PerformanceFilter implements Filter { ...

  如果使用web.xml设置过滤器,则可以在<servlet>中设置<async-supported>标签为true。

复制代码
1
2
3
4
5
<filter> <filter-name>PerformanceFilter</filter-name> <filter-class>com.filter.PerformanceFilter</filter-class> <async-supported>true</async-supported> </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
31
32
33
34
35
36
37
38
39
40
41
42
@WebFilter( filterName="PerformanceFilter", asyncSupported=true, urlPatterns={"/*"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC }, initParams = { @WebInitParam(name = "Site", value = "CSDN") }) public class PerformanceFilter implements Filter { private FilterConfig config; public PerformanceFilter() { // TODO Auto-generated constructor stub } public void destroy() { // TODO Auto-generated method stub } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long begin = System.currentTimeMillis(); chain.doFilter(request, response); //日志中记录本次Request请求消耗的时间 config.getServletContext().log("Performance process in " + (System.currentTimeMillis() - begin) + " milliseconds"); // 输出站点名称 System.out.println("do Filter has called"); } public void init(FilterConfig fConfig) throws ServletException { // 获取初始化参数 this.config = fConfig; String site = config.getInitParameter("Site"); // 输出初始化参数 System.out.println("PerformanceFilter init done! 初始变量值: " + site); } }

  在对ServletContext的监听中初始化线程池。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebListener public class AppContextListener implements ServletContextListener { public AppContextListener() { // TODO Auto-generated constructor stub } public void contextDestroyed(ServletContextEvent sce) { ExecutorService executor = (ExecutorService)sce.getServletContext().getAttribute("executor"); executor.shutdown(); } public void contextInitialized(ServletContextEvent sce) { // 创建线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100)); sce.getServletContext().setAttribute("executor", executor); } }

  完成耗时处理的工作任务实现。

复制代码
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
public class AsyncRequestProcessor implements Runnable { private AsyncContext asyncContext; private int secs; public AsyncRequestProcessor(AsyncContext asyncCtx, int secs) { this.asyncContext = asyncCtx; this.secs = secs; } @Override public void run() { System.out.println("Async Suppored?" + asyncContext.getRequest().isAsyncSupported()); longProcessing(secs); try { ServletResponse response = asyncContext.getResponse(); PrintWriter out = response.getWriter(); response.setContentType("text/html;charset=UTF-8"); out.println("<!DOCTYPE html PUBLIC " + "'-//W3C//DTD HTML 4.01 Transitional//EN'>"); out.println("<html>"); out.println("<head>"); out.println("<title>LongRunningServlet</title>"); out.println("</head>"); out.println("<body>"); out.write("<h1>" + Thread.currentThread().getName() + "</h1>"); out.write("<h1>Processing done for " + secs + " milliseconds!</h1>"); out.println("</body>"); out.println("</html>"); out.close(); } catch (IOException e) { e.printStackTrace(); } asyncContext.complete(); } private void longProcessing(int secs){ try { Thread.sleep(secs); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } }

  请求与响应都封装在AsyncContext中,即请求上下文是通过AsyncContext实例来保存的,它持有容器提供的请求与响应对象。所以AsyncRequestProcessor构建时必须接受AsyncContext实例。以上工作任务AsyncRequestProcessor的实现中,以暂停线程的方式来模拟长时间处理,并输出简单的HTML流来作为响应。

  异步处理监听器的实现。

复制代码
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
@WebListener public class AppAsyncListener implements AsyncListener { public AppAsyncListener() { // TODO Auto-generated constructor stub } public void onComplete(AsyncEvent event) throws java.io.IOException { System.out.println("AppAsyncListener onComplete"); } public void onError(AsyncEvent event) throws java.io.IOException { System.out.println("AppAsyncListener onError"); } public void onStartAsync(AsyncEvent event) throws java.io.IOException { System.out.println("AppAsyncListener onStartAsync"); } //通知的实现在 Timeout()方法,通过它发送超时响应给客户端 public void onTimeout(AsyncEvent event) throws java.io.IOException { System.out.println("AppAsyncListener onTimeOut"); ServletResponse response = event.getAsyncContext().getResponse(); PrintWriter out = response.getWriter(); out.write("TimeOut Error in Processing"); } }

  异步Servlet实现,注意使用AsyncContext和ThreadPoolExecutor进行处理。

复制代码
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
@WebServlet( urlPatterns={"/AsyncLongRunningServlet"}, asyncSupported=true ) public class AsyncLongRunningServlet extends HttpServlet { private static final long serialVersionUID = 1L; public AsyncLongRunningServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); System.out.println("AsyncLongRunningServlet Start::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId()); request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true); String time = request.getParameter("time"); if(null == time) time = "1"; int secs = Integer.valueOf(time); if(secs > 10000) secs = 10000; //请求上下文是通过AsyncContext实例来保存的,它持有容器提供的请求与响应对象。 //使用startAsync()方法取得AsyncContext对象后,此次请求的响应会被延后, //并释放容器分配给该请求的线程 AsyncContext asyncCtx = request.startAsync(); asyncCtx.addListener(new AppAsyncListener()); //设置超时响应时间为9000ms //浏览器中URL中time=5500不超时 //time=9500就会提示超时 asyncCtx.setTimeout(9000); ExecutorService executor = (ExecutorService)request .getServletContext().getAttribute("executor"); executor.execute(new AsyncRequestProcessor(asyncCtx, secs)); long endTime = System.currentTimeMillis(); System.out.println("AsyncLongRunningServlet End::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId() + "::Time Taken=" + (endTime - startTime) + " millseconds."); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }

  在浏览器上请求URL:http://localhost:8088/AsyncServletTest/AsyncLongRunningServlet?time=3000,得到响应如图所示:

  查看控制台中打印的信息如下:

复制代码
1
2
3
4
5
6
7
AsyncLongRunningServlet Start::Name=http-nio-8088-exec-3::ID=30 AsyncLongRunningServlet End::Name=http-nio-8088-exec-3::ID=30::Time Taken=6 millseconds. 站点网址:http://www.csdn.net/ 六月 20, 2017 7:48:32 下午 org.apache.catalina.core.ApplicationContext log 信息: Performance process in 6 milliseconds Async Suppored?true AppAsyncListener onComplete

  如果运行时设置time=10000,在客户端超时以后会得到响应超时错误处理和日志:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AsyncLongRunningServlet Start::Name=http-nio-8088-exec-7::ID=35 AsyncLongRunningServlet End::Name=http-nio-8088-exec-7::ID=35::Time Taken=0 millseconds. 站点网址:http://www.csdn.net/ 六月 20, 2017 8:02:11 下午 org.apache.catalina.core.ApplicationContext log 信息: Performance process in 0 milliseconds Async Suppored?true AppAsyncListener onTimeOut AppAsyncListener onComplete Exception in thread "pool-1-thread-2" java.lang.IllegalStateException: The request associated with the AsyncContext has already completed processing. at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:497) at org.apache.catalina.core.AsyncContextImpl.getResponse(AsyncContextImpl.java:219) at process.AsyncRequestProcessor.run(AsyncRequestProcessor.java:32) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)

  异步Servlet的解决方案非常适合于某些应用场景,比如说群组通知与拍卖价格通知等。不过,对于等待数据库查询完成的请求来说,这种方式就没有什么必要了。 对于那些不适合于这种解决方案的场景来说,我还是要说一下这种方式的好处。除了在吞吐量与延迟方面带来的显而易见的改进外,这种方式还可以在大负载的情况下优雅地避免可能出现的线程饥饿问题。另一个重要的方面,这种异步处理请求的方式已经是标准化的了。它不依赖于你所使用的Servlet API 3.0,兼容于各种应用服务器,如Tomcat 7、JBoss 6或是Jetty 8等,在这些服务器上这种方式都可以正常使用。

  需要特别注意的是在不支持异步处理的Servlet或Filter中调用startAsync(),会抛出IllegalStateException。当在支持异步处理的Servlet或Filter中调用请求对象的startAsync()方法时,该次请求会离开容器所分配的线程,这意味着响应处理流程会返回,也就是若有过滤器,也会依序返回,但最终的响应被延迟。

  可以调用AsyncContext的complete()方法完成响应或者调用forward()方法,将响应转发给别的Servlet/JSP处理,AsyncContext的forward()将请求的响应权派送给别的页面来处理,给定的路径是相对于ServletContext的路径。不可以在同一个AsyncContext中同时调用complete()与forward()方法,否则会抛出IllegalStateException

3、模拟服务器推播

  HTTP是基于请求/响应模型的,HTTP服务器无法直接对客户端传送信息,因为没有请求就不会有响应。在这种请求/响应模型下,如果客户端想获得服务器端应用程序的最新状态,就必须定期(或不定期)的方式发送请求,查询服务器的最新状态。

  持续发送请求以查询服务器端最新状态,这种方式的问题在于耗用网络流量,如果多次请求过程后,服务器应用程序状态并没有变化,那这多次的请求耗用的流量就是浪费的。一个解决方式是,服务器端将每次请求的响应延后,直到服务器端应用程序状态有变化时再进行响应。这样的话,客户端将会处于等待响应的状态,如果是浏览器,可以搭配Ajax异步请求技术,而用户将不会因此而被迫停止网页的操作。然而服务器端延后请求的话,若是Servlet/JSP技术,等于该请求占用一个线程,若客户端很多,每个请求都占用线程,将会使服务端的性能负担很重。

  Servlet 3.0中提供的异步处理技术,可以解决每个请求占用线程的问题,若搭配浏览器端Ajax异步请求技术,就可达到类似服务器端主动能和浏览器的行为,也就是所谓的服务器推播(Server Push)。

  以下是实例的例子,模拟应用程序不定期产生最新数据。这个部分由实现ServletContextLister接口的类来负责。

复制代码
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
@WebListener public class WebInitListener implements ServletContextListener { //所有异步请求的AsyncContext将存储在这个List中 private List<AsyncContext> asyncs = new ArrayList<AsyncContext>(); public WebInitListener() { } public void contextInitialized(ServletContextEvent sce) { sce.getServletContext().setAttribute("asyncs", asyncs); new Thread(new Runnable() { @Override public void run() { while(true) { try { Thread.sleep((int)(Math.random() * 10000)); double num = Math.random() * 10; synchronized (asyncs) { for(AsyncContext ctx : asyncs) { ctx.getResponse().getWriter().println(num); ctx.complete(); } asyncs.clear(); } } catch (Exception e) { throw new RuntimeException(e); } } } }).start(); } public void contextDestroyed(ServletContextEvent sce) { } }

  在这个ServletContextListener中,有一个List会存储所有异步请求的AsyncContext,并在不定时的产生数字后,逐一对客户端响应,并调用AsyncContext的complete()来完成请求。

  负责接受请求的Servlet,一收到请求,就将之加入到List中。

复制代码
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
@WebServlet(name = "AsyncNumServlet", urlPatterns = {"/asyncNum.do"}, asyncSupported = true) public class AsyncNumServlet extends HttpServlet { private static final long serialVersionUID = 1L; private List<AsyncContext> asyncs; public AsyncNumServlet() { super(); } @SuppressWarnings("unchecked") @Override public void init() throws ServletException { asyncs = (List<AsyncContext>) getServletContext().getAttribute("asyncs"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub AsyncContext ctx = request.startAsync(); synchronized (asyncs) { //加入到维护AsyncContext的List中 asyncs.add(ctx); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }

  由于维护AsyncContext的List存储为ServletContext的属性,所以在这个Servelt中,必须从ServletContext中取出该List,在每次请求到来时,调用HttpServletRequest的startAsync()进行异步处理,并将取得的AsyncContext加入至维护AsyncContext的List中。

  可以使用一个简单的HTML,其中使用AJAX技术,发送异步请求至服务器端,这个请求会被延迟,直到服务器端完成响应后,更新网页上的数据,并再度发送异步请求。async.html文件如下:

复制代码
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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>实时数据更新</title> </head> <body> <p> 实时数据更新:<span id="data">0</span> </p> <script type="text/javascript"> //下面是AJAX的JS原生写法 function asyncUpdate() { var xhr = null; if(window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else if(window.ActiveXObject) { xhr = new ActiveXObject('Microsoft.XMLHTTP'); } xhr.onreadystatechange = function () { if (xhr.readyState === 4) { //4表示解析完毕 // 判断响应结果: if (xhr.status === 200) { // 成功,通过responseText拿到响应的文本: document.getElementById('data').innerHTML = xhr.responseText; asyncUpdate(); } } }; xhr.open('GET', 'asyncNum.do?timestamp=' + new Date().getTime()); xhr.send(null); } window.onload = asyncUpdate; </script> </body> </html>

  上面HTML文件中,AJAX的写法是原生的javascript写法,jQuery的写法如下:

复制代码
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
<head> ... <script type="text/javascript" src="static/jquery-1.12.4.min.js"></script> ... </head> ... <script type="text/javascript"> //下面是AJAX的Jquery写法 $(function () { console.log("ready执行"); function asyncUpdate() { /*$.get('asyncNum.do?timestamp=' + new Date().getTime(), function(data) { // 成功,拿到响应的文本: $("#data").text(data); //返回的data是字符串类型 asyncUpdate(); });*/ $.get('asyncNum.do', {'timestamp' : new Date().getTime()}, function(data) { // 成功,拿到响应的文本: $("#data").text(data); //返回的data是字符串类型 asyncUpdate(); }); } asyncUpdate(); }); </script>

  在浏览器上请求URL:http://localhost:8088/AsyncServerPushDemo/async.html,得到响应如图所示:

  可以试着用多个浏览器窗口来请求这个页面,你会看到每个浏览器窗口的数据都是同步更新的。就像下面这样:

最后

以上就是跳跃蜜粉最近收集整理的关于Servlet异步处理特性分析与实践的全部内容,更多相关Servlet异步处理特性分析与实践内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(81)

评论列表共有 0 条评论

立即
投稿
返回
顶部