為什麼要異步編程
在現在大規模高並發的 WEB 應用中,由於硬件及網絡的限制,I/O 處理速度相當較慢,往往 成為 WEB 系統的性能瓶頸。Node.js 通過非阻塞的 IO 和事件驅動很好的緩解了 Web 服務器在高並發時的資源占用,極大 的提高了 WEB 服務器對高並發的處理能力。同時 Node.js 帶來的輕量快捷的異步編程給 WEB 開發帶來了一股清新的空氣 。那麼對於廣大的 Java 開發者來說,是否可以實現類似的 WEB 開發呢,答案是肯定的。利用 Groovy 動態語言、Jetty 等輕量的 WEB 服務器,可以很方便的進行 WEB 的異步編程。
Groovy 簡介
Groovy 是一個基於 Java 虛擬機 的動態語言。Groovy 代碼能夠與 Java 代碼很好地結合。為 Java 開發者提供了現代流行的動態編程語言特性,而且學習 成本很低。目前最新的發布版本是 1.8. 我們下面示例的代碼就是使用這一版本。
Jetty 簡介
Jetty 是一個 用 Java 實現、開源、基於標准的,並且具有豐富功能的 HTTP 服務器和 Web 容器,可以免費的用於商業。現在已經有非 常多的成功產品基於 Jetty,比如 Apache Geromino、Eclipse、Google AppEngine 等。Jetty 可以用來作為一個傳統的 Web 服務器,也可以作為一個動態的內容服務器,並且 Jetty 可以非常容易的嵌入到 Java 應用程序當中。目前最新版是 8.1. 我們下面示例的代碼就是使用這一版本。
怎樣使用 Jetty 快速開發輕量 WEB 服務
Jetty 可以非常方 便的嵌入到應用程序當中,就像使用一般的類庫一樣。示例代碼如下:
清單 1. 用 Jetty 快速開發輕量 WEB 服務
class DefaultHandler extends AbstractHandler { void handle(String target, Request baseRequest, HttpServletRequest request,HttpServletResponse response) { response.contentType = "text/html;charset=utf-8" response.status = HttpServletResponse.SC_OK baseRequest.handled = true response.writer.println "<h1>Hello World!</h1>" } } Server server = new Server( 8080 ) server.setHandler( new DefaultHandler() ) server.start() server.join()
從代碼我們可以看到先實例化一個 Server 對象,並指定 Server 監聽的端口是 8080。然後為 Server 設定一個處理 Web 請求的 Handler,我們這裡是 DefaultHandler,最後調用 start 方法啟動 Server。這樣一個 簡單的 Web 服務器就可以使用了,只需要簡單的幾行代碼。
在浏覽器中訪問 URL:http://localhost:8080,會得 到下面的響應 :
圖 1. Web 頁面響應
Jetty 中 Servlet 編程
在 Jetty 中 加入 Servlet 也很方便,下面代碼演示了怎樣在 Jetty 中用 Servlet 提供 Web 服務。我們只需要將 handler 換成 ServletContextHandler 就可以了。
清單 2. Jetty 中 Servlet 編程
import javax.servlet.http.* import org.eclipse.jetty.server.* import org.eclipse.jetty.servlet.* class HelloWorldServlet extends HttpServlet { void doGet(HttpServletRequest request, HttpServletResponse response) { response.contentType = "text/html;charset=utf-8" response.status = HttpServletResponse.SC_OK response.writer.println "<h1>Hello World ! </h1>" } } Server server = new Server(8080) ServletContextHandler context = new ServletContextHandler(\ ServletContextHandler.SESSIONS ) context.contextPath = "/" server.setHandler(context) context.addServlet( new ServletHolder(new HelloWorldServlet()),"/*") server.start() server.join()
異步 Servlet 編程
為了應對 Ajax 應用,大並發應用對服務器造成的壓力,我們知道 Node.js 利用事件機制 異步編程使這一問題得到了很大的緩解。那麼 J2EE 對這一問題是怎樣支持的呢, Servlet3.0 推出的異步 Servlet 就是 為了解決這個問題的。目前已經有不少支持 Servlet 3.0 的 Web 服務器,如 GlassFish v3、Tomcat 7.0、Jetty 8.0 等 。 實際上 Jetty 在 Servlet3.0 發布之前已經開始支持異步機制:Continuations 特性。隨著 Servlet3.0 的發布, Jetty 的異步支持也更新為異步 Servlet 的支持了。而且 Jetty 默認就是將異步支持特性打開的。
下面我們看看 怎樣進行異步 Servlet 編程 :
清單 3. 異步 servlet 編程
import javax.servlet.* import javax.servlet.http.* import org.eclipse.jetty.server.Server import org.eclipse.jetty.servlet.* class AsyncTestServlet extends HttpServlet { void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.contentType = "text/html;charset=UTF-8" def out = response.writer; out.println "進入 Servlet 的時間:${ new Date() }.<br/>" out.flush() // 在子線程中執行業務調用,並由其負責輸出響應,主線程退出 AsyncContext ctx = request.startAsync() new Thread(new BusinessExecutor(ctx)).start() out.println "結束 Servlet 的時間:${ new Date() }.<br/>" out.flush() } } Server server = new Server(8080) ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS ) context.contextPath = "/" server.setHandler context context.addServlet(new ServletHolder(new AsyncTestServlet()),"/*") server.start() server.join()
大家可以看到代碼和上面的傳統 Servlet 編程基本差不多。異步 Servlet 處理首先要調用 request.startAsync() 得到 AsyncContext。並將 AsyncContext 傳給異步業務處理,以便當業務完成時進行 WEB 響應。 在這裡我們簡單的調用一個線程進行業務處理。而 Servlet 則不會像傳統的 Servlet 那樣被業務處理所堵塞。Servlet 線 程會直接返回,並被 Server 回收放回緩沖池。從而節約 WEB 服務器資源,提高服務器處理 WEB 請求的能力。
下 面是業務處理的代碼:
清單 4. 業務處理
class BusinessExecutor implements Runnable { AsyncContext ctx BusinessExecutor(AsyncContext ctx){ this.ctx = ctx } void run(){ // 等待 3 秒鐘,以模擬業務的執行 Thread.sleep(3000) def out = ctx.getResponse().getWriter() out.println "業務處理完成的時間:${ new Date() }.<br/>" out.flush() ctx.complete() } }
我們這裡只是讓線程等待 3 秒鐘。然後從 AsyncContext 得到 Response 進行 Web 響應,最後調用 AsyncContext.complete() 告訴 Server 完成響應。
訪問 Server 我們可以得到如下響應:
圖 2. 異步 Servlet 的響應
可以看到 Servlet 先結束返回,然後業務處理異步執行完後才輸出響應返回。
簡化異步處理
為了 更方便的進行業務的異步處理以及提高初始化線程的效率,我們可以提供一個異步處理的工具類。
清單 5. 異步處 理工具類
import java.util.concurrent.* class ConcurrentUtils { static ExecutorService executor static ExecutorService getExecutorService(){ if( executor ==null ){ executor = Executors.newCachedThreadPool() } return executor } static Future callAsync(final Closure cl, final Object... args){ return getExecutorService().submit( new Callable(){ public Object call()throws Exception{ return cl.call(args) } } ) } static shutdown(){ if( executor !=null ){ executor.shutdown() } } }
我們利用 Java Concurrent 包提供的 ExecutorService 初始化一個線程緩沖池。以提高生成線程的效率。利 用這個工具類我們的異步 Servlet 編程如下,我們可以發現代碼會更加簡單。
清單 6. 簡化後的異步編程
class AsyncUtilsServlet extends HttpServlet { void doGet(HttpServletRequest request, HttpServletResponse response) { response.contentType = "text/html;charset=UTF-8" def out = response.writer out.println "進入 Servlet 的時間:${ new Date() }.<br/>" out.flush(); // 在子線程中執行業務並輸出響應 AsyncContext ctx = request.startAsync() ConcurrentUtils.callAsync { out = ctx.getResponse().getWriter() out.println "業務處理開始的時間:${ new Date() }.<br/>" out.flush() Thread.currentThread().sleep( 5000 ) out.println "業務處理完畢的時間:${ new Date() }.<br/>" out.flush() ctx.complete() } out.println "結束 Servlet 的時間:${ new Date() }.<br/>" out.flush() } }
將同步 Servlet 轉化為異步 Servlet
到這裡我們已經演示了怎樣用 Groovy 進行異步 Servlet 的編程 。那麼以前實現的傳統的 Servlet 有沒有辦法轉化為異步 Servlet 呢?我們下面做一個簡單的嘗試。
為了將傳統 的 Servlet 轉化為異步 Servlet,我們需要在調用真正的 Servlet 之前調用異步支持。下面我們用一個 AsyncServletAdapter 類將傳統 Servlet 包裝成異步的 Servlet。
清單 7. AsyncServletAdapter
import javax.servlet.* import javax.servlet.http.* class AsyncServletAdapter extends HttpServlet { Servlet servlet AsyncServletAdapter(Servlet servlet) { super() this.servlet = servlet } void service(ServletRequest request, ServletResponse response) { response.contentType = "text/html;charset=UTF-8" def out = response.writer; out.println "start call servlet 的時間:${ new Date() }.<br/>" AsyncContext ctx = request.startAsync() ConcurrentUtils.callAsync { try { if( servlet!=null ) servlet.service( ctx.getRequest(), ctx.getResponse() ) }catch( Throwable e ){ out.print(e) } finally { ctx.complete() } } out.println "end call Servlet 的時間:${ new Date() }.<br/>" out.flush() } }
為簡化編程我們也將 Jetty 的 ServletHolder 簡單包裝一下。
清單 8. ServletHolder 包裝
class AsyncServletHolder extends ServletHolder { AsyncServletHolder(Servlet servlet) { super(new AsyncServletAdapter( servlet) ) } AsyncServletHolder(Closure closure) { super(new AsyncServletAdapter( closure) ) } }
那麼傳統同步的 Servlet 轉化為異步 Servlet 的代碼如下:
清單 9. 同步的 Servlet 轉化為異步 Servlet
import javax.servlet.* import javax.servlet.http.* import org.eclipse.jetty.server.Server import org.eclipse.jetty.servlet.* import com.ibm.asyncweb.core.AsyncServletHolder class TestServlet extends HttpServlet { void doGet(HttpServletRequest request, HttpServletResponse response){ def out = response.writer out.println "進入 Servlet 的時間:${ new Date() }.<br/>" out.flush() // 執行業務並輸出響應 Thread.currentThread().sleep( 3000 ) out.println "業務處理完畢的時間:${ new Date() }.<br/>" out.flush() out.println "結束 Servlet 的時間:${ new Date() }.<br/>" out.flush() } } Server server = new Server(8080) ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS ) context.setContextPath("/") server.setHandler(context) context.addServlet(new AsyncServletHolder(new TestServlet()),"/*") server.start() server.join()
這樣傳統的 Servlet 就自動的轉化為異步 Servlet 了。大家也可以將這個思路應用到傳統配置型 的 Web 容器上,自動的將傳統同步的 Servlet 轉化為異步 Servlet.