异步Servlet示例

时间:2020-02-23 14:36:08  来源:igfitidea点击:

Servlet 3中引入了异步Servlet。
这是处理长时间运行的线程的线程匮乏问题的好方法。

异步Servlet

在深入了解什么是异步Servlet之前,让我们先尝试一下为什么需要它。

假设我们有一个Servlet,处理时间很长,如下所示。

package com.theitroad.servlet;

import java.io.IOException;
import java.io.PrintWriter;

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("/LongRunningServlet")
public class LongRunningServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	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());

		String time = request.getParameter("time");
		int secs = Integer.valueOf(time);
		//max 10 seconds
		if (secs > 10000)
			secs = 10000;

		longProcessing(secs);

		PrintWriter out = response.getWriter();
		long endTime = System.currentTimeMillis();
		out.write("Processing done for " + secs + " milliseconds!!");
		System.out.println("LongRunningServlet Start::Name="
				+ Thread.currentThread().getName() + "::ID="
				+ Thread.currentThread().getId() + "::Time Taken="
				+ (endTime - startTime) + " ms.");
	}

	private void longProcessing(int secs) {
		//wait for given time before finishing
		try {
			Thread.sleep(secs);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

如果我们通过浏览器在上述servlet上使用URL作为https://localhost:8080/AsyncServletExample/LongRunningServlet?time = 8000,则响应为"处理完成8000毫秒!" 8秒后。
现在,如果您查看服务器日志,将得到以下日志:

LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103
LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103::Time Taken=8002 ms.

因此,尽管大多数处理与Servlet请求或者响应无关,但我们的Servlet线程运行了大约8+秒。

这可能导致线程饥饿-因为我们的servlet线程被阻塞,直到完成所有处理为止。
如果服务器收到大量要处理的请求,它将达到最大servlet线程限制,并且进一步的请求将出现Connection Refused错误。

在Servlet 3.0之前,有针对这些长时间运行的线程的特定于容器的解决方案,我们可以生成一个单独的工作线程来执行繁重的任务,然后将响应返回给客户端。
启动工作线程后,Servlet线程返回到Servlet池。
Tomcat的Comet,WebLogic的FutureResponseServlet和WebSphere的异步请求分派器是异步处理实现的一些示例。

特定于容器的解决方案的问题在于,我们必须在不更改应用程序代码的情况下才能迁移到其他servlet容器,这就是为什么Servlet 3.0中添加了Async Servlet支持以为Servlet中的异步处理提供标准方式的原因。

异步Servlet实现

让我们看一下实现异步servlet的步骤,然后为上述示例提供异步支持的servlet。

  • 首先,我们要提供异步支持的servlet应该具有@WebServlet批注,其asyncSupported值为true。

  • 由于实际工作将委托给另一个线程,因此我们应该有一个线程池实现。
    我们可以使用Executors框架创建线程池,并使用servlet上下文侦听器启动线程池。

  • 我们需要通过ServletRequest.startAsync()方法获取AsyncContext的实例。
    AsyncContext提供了获取ServletRequest和ServletResponse对象引用的方法。
    它还提供了使用dispatch()方法将请求转发到另一个资源的方法。

  • 我们应该有一个Runnable实现,在那里我们将进行繁重的处理,然后使用AsyncContext对象将请求分派到另一个资源,或者使用ServletResponse对象写入响应。
    处理完成后,我们应调用AsyncContext.complete()方法以使容器知道异步处理已完成。

  • 我们可以将AsyncListener实现添加到AsyncContext对象中以实现回调方法–我们可以使用它为异步线程处理过程中发生错误或者超时的情况提供对客户端的错误响应。
    我们还可以在此处进行一些清理活动。

一旦我们完成了异步Servlet示例的项目,它将如下图所示。

在Servlet上下文侦听器中初始化辅助线程池

package com.theitroad.servlet.async;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class AppContextListener implements ServletContextListener {

	public void contextInitialized(ServletContextEvent servletContextEvent) {

		//create the thread pool
		ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L,
				TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100));
		servletContextEvent.getServletContext().setAttribute("executor",
				executor);

	}

	public void contextDestroyed(ServletContextEvent servletContextEvent) {
		ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent
				.getServletContext().getAttribute("executor");
		executor.shutdown();
	}

}

该实现非常简单,如果您不熟悉Executors框架,请阅读Thread Pool Executor。

有关侦听器的更多详细信息,请浏览Servlet侦听器示例。

工作线程实现

package com.theitroad.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.AsyncContext;

public class AsyncRequestProcessor implements Runnable {

	private AsyncContext asyncContext;
	private int secs;

	public AsyncRequestProcessor() {
	}

	public AsyncRequestProcessor(AsyncContext asyncCtx, int secs) {
		this.asyncContext = asyncCtx;
		this.secs = secs;
	}

	@Override
	public void run() {
		System.out.println("Async Supported? "
				+ asyncContext.getRequest().isAsyncSupported());
		longProcessing(secs);
		try {
			PrintWriter out = asyncContext.getResponse().getWriter();
			out.write("Processing done for " + secs + " milliseconds!!");
		} catch (IOException e) {
			e.printStackTrace();
		}
		//complete the processing
		asyncContext.complete();
	}

	private void longProcessing(int secs) {
		//wait for given time before finishing
		try {
			Thread.sleep(secs);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

请注意,AsyncContext的使用及其在获取请求和响应对象,然后通过complete()方法调用完成异步处理的用法。

AsyncListener实现

package com.theitroad.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebListener;

@WebListener
public class AppAsyncListener implements AsyncListener {

	@Override
	public void onComplete(AsyncEvent asyncEvent) throws IOException {
		System.out.println("AppAsyncListener onComplete");
		//we can do resource cleanup activity here
	}

	@Override
	public void onError(AsyncEvent asyncEvent) throws IOException {
		System.out.println("AppAsyncListener onError");
		//we can return error response to client
	}

	@Override
	public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
		System.out.println("AppAsyncListener onStartAsync");
		//we can log the event here
	}

	@Override
	public void onTimeout(AsyncEvent asyncEvent) throws IOException {
		System.out.println("AppAsyncListener onTimeout");
		//we can send appropriate response to client
		ServletResponse response = asyncEvent.getAsyncContext().getResponse();
		PrintWriter out = response.getWriter();
		out.write("TimeOut Error in Processing");
	}

}

请注意onTimeout()方法的实现,在该方法中,我们向客户端发送超时响应。

异步Servlet示例实现

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

package com.theitroad.servlet.async;

import java.io.IOException;
import java.util.concurrent.ThreadPoolExecutor;

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(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
public class AsyncLongRunningServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	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");
		int secs = Integer.valueOf(time);
		//max 10 seconds
		if (secs > 10000)
			secs = 10000;

		AsyncContext asyncCtx = request.startAsync();
		asyncCtx.addListener(new AppAsyncListener());
		asyncCtx.setTimeout(9000);

		ThreadPoolExecutor executor = (ThreadPoolExecutor) 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) + " ms.");
	}

}

运行异步Servlet Web应用程序

现在,当我们在Servlet上以URL作为https://localhost:8080/AsyncServletExample/AsyncLongRunningServlet?time = 8000 运行时,我们得到的响应和日志如下:

AsyncLongRunningServlet Start::Name=http-bio-8080-exec-50::ID=124
AsyncLongRunningServlet End::Name=http-bio-8080-exec-50::ID=124::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onComplete

如果我们将时间设置为9999,则会发生超时,并在客户端以"处理中的超时错误"和日志形式获得响应:

AsyncLongRunningServlet Start::Name=http-bio-8080-exec-44::ID=117
AsyncLongRunningServlet End::Name=http-bio-8080-exec-44::ID=117::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onTimeout
AppAsyncListener onError
AppAsyncListener onComplete
Exception in thread "pool-5-thread-6" java.lang.IllegalStateException: The request associated with the AsyncContext has already completed processing.
	at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:439)
	at org.apache.catalina.core.AsyncContextImpl.getResponse(AsyncContextImpl.java:197)
	at com.theitroad.servlet.async.AsyncRequestProcessor.run(AsyncRequestProcessor.java:27)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
	at java.lang.Thread.run(Thread.java:680)

请注意,Servlet线程快速完成了执行,并且所有主要处理工作都在其他线程中进行。