Java Web基礎 --- Servlet 綜述(實踐篇)

書呆子Rico發表於2017-03-09

摘要:

  伴隨 J2EE 6一起釋出的Servlet 3.0規範是Servlet規範歷史上最重要的變革之一,它的許多新的特性都極大的簡化了 Java Web 應用的開發。本文從一個簡單的 Servlet 例子開始,說明了如何開發、配置一個 Servlet。此外,還重點敘述了Servlet的一些新特性,包括Servlet 非同步處理、Servlet 非阻塞IO 以及 Servlet 檔案上傳等內容,以便我們對Servlet有一個更全面的瞭解。


  本篇主要介紹 Servlet 實踐方面的知識,更多關注於Servlet的新特性:

  • Servlet 例項;
  • Servlet 配置;
  • Servlet 非同步處理;
  • Servlet 非阻塞IO;
  • Servlet 檔案上傳。

  更多關於Servlet理論方面的介紹見我的上一篇博文《Servlet 綜述(理論篇)》,其具體包括以下幾個方面的內容:

  • 為什麼會有 Servlet;
  • Servlet 是什麼;
  • Servlet 如何實現預期效果;
  • Servlet 的作用原理;
  • Servlet 與 併發;
  • Servlet 與 Java Web 應用的結構演變歷程;
  • Servlet 與 MVC 的聯絡。

版權宣告:

本文原創作者:書呆子Rico
作者部落格地址:http://blog.csdn.net/justloveyou_/


一. 從一個簡單的 Servlet 例子說起

  我們看下面這個簡單的示例:

Servlet:

public class TestServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {

        //獲取請求引數
        String param1 = request.getParameter("name");
        String param2 = request.getParameter("gentle");

        //獲取Servlet引數並放到request中
        String age = this.getServletConfig().getInitParameter("age");
        request.setAttribute("age",age);


        // 此處進行業務邏輯處理


        //根據處理結果轉發到相應的表現層進行顯示
        request.getRequestDispatcher("/showInfo.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doGet(req, resp);
    }
}

web.xml 配置檔案片段:

<context-param>
    <param-name>campus</param-name>
    <param-value>NEU</param-value>
</context-param>
<servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>com.edu.tju.rico.servlet.TestServlet</servlet-class>
    <init-param>
        <param-name>age</param-name>
        <param-value>24</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/servlet/test</url-pattern>
</servlet-mapping>

顯示邏輯:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<html>
<head>
<title>showInfo</title>
</head>
<body>
    請求引數: Name:&nbsp;&nbsp;<%= request.getParameter("name")%><br>
    Gentle:&nbsp;&nbsp;<%= request.getParameter("gentle")%><br>
    <br> 
    ----------------我是分割線--------------------<br> 
    <br> 
    Web應用初始化引數: &nbsp;&nbsp;<%= application.getInitParameter("campus")%><br>
    <br> 
    ----------------我是分割線--------------------<br> 
    <br> 
    TestServlet 初始化引數: &nbsp;&nbsp;${requestScope.age}<br>
    <br>
</body>
</html>

  開發一個Servlet程式時,如果其是基於HTTP協議的,那麼我們一般繼承 HttPServlet 抽象類並重寫 doGet() 和 doPost() 方法,或者直接重寫 service() 方法去處理Http請求。

  更多關於 JSP技術的細節見我的其他兩篇部落格: 《Java Web基礎 — Jsp 綜述(上)》《Java Web基礎 — Jsp 綜述(下)》


二. Servlet 的配置

  為了讓 Servlet 能夠響應使用者請求,還必須將 Servlet 配置到我們的Web應用中。進一步地,如果我們沒有為Servlet配置URL,那麼該Servlet將不能響應使用者請求。從 J2EE 6 (Servlet 3.0) 開始,配置 Servlet 的方式共有兩種:

  • 在 web.xml 中進行配置;

  • 在對應的Servlet類中使用@WebServlet註解進行配置。


  我們在這裡主要說明使用@WebServlet註解進行配置Servlet,使用 web.xml 配置的方法與該種方式只是在形式不同,作用方式是一樣的,此不贅述。

  一旦我們使用 @WebServlet 配置了Servlet,那我們就不用在 web.xml 進行再次配置了,並且不能在web.xml中將 metadata-complete 屬性設定為true。 支援的常用屬性如下表所示:

屬性名 是否必需 型別 描述
name String 指定 Servlet 的 name 屬性,等價於<servlet-name>標籤,預設取值為Servlet類的全限定名
value String[] 該屬性等價於 urlPatterns 屬性,這兩個屬性不能同時使用
loadOnStartup int 指定Servlet的載入時機和順序,等價於<load-on-startup>標籤
initParam WebInitParam[] 指定一組 Servlet 初始化引數,等價於<init-param>標籤
asyncSupported boolean 宣告 Servlet 是否支援非同步操作模式,等價於<async-supported>標籤
description String 該Servlet的描述資訊,等價於<description>標籤
displayName String 該Servlet的顯示名,通常配合工具使用,等價於<display-name>標籤

  將上述示例使用@WebServlet註解配置,如下:

@WebServlet(name = "Test", 
    urlPatterns = { "/servlet/test" }, 
    initParams = { @WebInitParam(name = "age", value = "24") })
public class TestServlet extends HttpServlet {
    ...
}

三. Servlet的新特性:非同步處理

1、Servlet 不支援非同步處理會帶來哪些痛點?

  在本篇的姊妹篇《Java Web基礎 — Servlet 綜述(實踐篇)》中,我們已經提到Servlet容器處理請求的方式。對於每個到達Web容器的請求,Web容器會為其分配一條執行執行緒來專門負責該請求,直到迴應完成前,該執行執行緒都不會被釋放回Web容器的執行緒池。 我們知道,執行執行緒會耗用系統資源,若某些請求需要長時間處理(例如長時間運算、等待某個資源),就會長時間佔用執行執行緒,若這類的請求很多,許多執行執行緒都被長時間佔用,對於整個系統而言就會是個效能負擔,甚至造成應用的效能瓶頸。

  特別地,基本上一些需長時間處理的請求,通常客戶端也較不在乎請求後要有立即的迴應,若可以,讓這類請求先釋放容器分配給該請求的執行執行緒,讓容器可以有機會將執行執行緒資源分配給其它的請求,這樣可以減輕系統負擔。這樣,原先釋放了容器所分配執行執行緒的請求,其迴應將被延後,直到處理完成(例如長時間運算完成、所需資源已獲得)再行對客戶端的迴應。


2、如何使用 Servlet 3.0 去支援對耗時事務的非同步處理?

1)、Servlet 3.0 對非同步處理的支援

  在 Servlet 3.0 之前的規範中,如果Servlet作為控制器呼叫了一個耗時的業務方法,那麼 Servlet 必須等到業務方法完全返回之後才能生成響應,這使得 Servlet 對業務方法的呼叫是一種阻塞式呼叫,因此效率比較低。Servlet 3.0 規範引入了非同步處理來解決問題,非同步處理允許Servlet重新發起一個執行緒去呼叫耗時的業務方法,這樣就可以避免等待。

  Servlet 3.0 的非同步處理是通過 AsyncContext 類來處理的,Servlet 可以通過 ServletRequest 的如下兩個方法開啟非同步呼叫、建立 AsyncContext 物件。在這裡,AsyncContext 物件代表非同步處理的上下文。

  • AsyncContext startAsync()
  • AsyncContext startAsync(ServletRequest,ServletRequest)

  這兩個方法都會返回 AsyncContext 物件,前者會直接利用原有的請求與響應物件來建立AsyncContext物件,後者則允許你傳入自行建立的請求、響應物件。 在呼叫了startAsync()方法取得AsyncContext物件之後,這次的響應就會被延後,並釋放容器所分配的執行執行緒。

  我們可以通過AsyncContext的getRequest()、 getResponse()方法取得請求、響應物件,此次對客戶端的響應將暫緩至呼叫AsyncContext的complete()方法或dispatch()方法為止,前者表示響應完成,後者表示將呼叫指定URL對應的內容進行響應。特別需要注意的是,dispatch()前後仍是同一個請求,並且被非同步請求dispatch的目標頁面必須指定:session=”false”。如果我們要支援 Servlet 的非同步處理,我們的 Servlet 就必須能夠支援非同步處理。也就是說,如果我們使用@WebServlet來標註的話,則必須將其asyncSupported屬性設為true,如下所示:

@WebServlet(urlPatterns = "/some.do", asyncSupported = true )   
public class AsyncServlet extends HttpServlet {   
...  
}

  特別需要注意的是,如果Servlet將支援非同步處理,並且其前端有過濾器,那麼過濾器也必須表明其支援非同步處理,如果使用@WebFilter註解的方式,同樣是需要設定其asyncSupported屬性為true,如下所示:

@WebFilter(urlPatterns = "/some.do", asyncSupported = true )   
public class AsyncFilter implements Filter{   
...
}

2)、非同步處理例項

(1) 進行非同步處理的Servlet類:

@WebServlet(urlPatterns = "/async",asyncSupported=true )
public class AsyncServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        request.setAttribute("param1", "我在非同步處理前被設定...");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println("<title>非同步呼叫示例</title>");
        out.println("進入Servlet的時間:" + new java.util.Date() + ".<br/>");

        //建立AsyncContext物件,開始非同步呼叫
        final AsyncContext async = request.startAsync();  // 在區域性(匿名)內部類直接使用,必須設為 final

        // 設定非同步呼叫的請求時長
        async.setTimeout(10 * 1000);

        // 啟動執行緒去處理耗時任務
        async.start(new Runnable() {   // 匿名內部類

            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    HttpServletRequest req = (HttpServletRequest) async
                            .getRequest();
                    req.setAttribute("param2", "我在耗時任務處理執行緒中被設定...");
                    async.dispatch("/async.jsp");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        out.println("結束Servlet的時間:" + new java.util.Date() + ".<br/>");
        out.flush();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doGet(req, resp);
    }
}

(2) 表現層:

<%-- 被非同步請求dispatch的目標頁面必須指定:session="false" --%>
<%@ page contentType="text/html; charset=utf-8" language="java" session="false"%>
<div style="background-color:#ffffdd;height:80px;">
    param1:${param1}<br />
    param2:${param2}<br />

    <%
        out.println("業務呼叫結束的時間:" + new java.util.Date());
    %>
</div>

(3) 結果展示頁面

          Servlet非同步處理.png-33.6kB


四、Servlet 3.1 支援非阻塞 IO

1、Servlet 不支援非阻塞 IO會帶來哪些痛點?

  Servlet 3.0 允許非同步請求處理,但僅限於傳統I/O,這大大限制了程式的可擴充套件性。我們知道,在應用程式中,一種典型的做法是,通過while迴圈讀取Servlet輸入流(Servlet InputStream),如下所示:

public class TestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws IOException, ServletException {     
 ServletInputStream input = request.getInputStream();
       byte[] b = new byte[1024];
       int len = -1;
       while ((len = input.read(b)) != -1) {
          . . . 
       }
   }
}

  
  事實上,Servlet 底層的 IO 是通過以下兩個 IO 流支撐的:

  • ServletInputStream:Servlet 用於讀取資料的輸入流;

  • ServletOutputStream:Servlet 用於輸出資料的輸出流。


  以 Servlet 讀取資料為例,傳統的讀取方式採用阻塞式IO —— 當Servlet讀取瀏覽器提交的資料時,如果資料暫時不可用,或者資料沒有讀取完成,Servlet當前所有執行緒將會被阻塞,無法繼續執行下去。另外,如果傳入的資料受到阻塞或流傳輸的速度慢於伺服器讀取的速度,則伺服器執行緒就需要一直等待資料的到來。同樣的情形在向Servlet輸出流(Servlet OutputStream)寫資料的時候也會出現。Servlet 3.1 提供的非阻塞IO進行輸入、輸出,可以更好地提升效能。


2、如何使用 Servlet 3.0 去支援非阻塞 IO?

  上述問題可以通過新增Servlet 3.1(JSR 340,作為Java EE7釋出的一部分)提供的事件監聽器ReadListener和WriteListener介面進行解決。通過 ServletInputStream.setReadListener 和 ServletOutputStream.setWriteListener 可以註冊監聽器。監聽器中提供了一些回撥方法,在資料讀寫不受阻塞的時候進行觸發。以 ReadListener 為例,實現ReadListener事件監聽器需要實現如下三個方法:

  • onDataAvailable():當有資料可用時激發該方法;

  • onAllDataRead():當所有資料讀取完成時激發該方法;

  • onError(Throwable t):讀取資料出現錯誤時激發該方法。


1)、Servlet 3.1 使用非阻塞IO步驟

  在 Servlet 3.1 中使用非阻塞IO步驟可分為三步:

(1) 呼叫 ServletRequest 的startAsync()方法開啟非同步處理模式;
(2) 通過 ServletRequest 獲取 ServletInputStream,併為 ServletInputStream 設定監聽器(ReadListener 實現類);
(3) 實現 ReadListener 介面來實現監聽器,在該監聽器的方法中以非阻塞方式讀取資料。

  改進後的doGet方法如下所示:

AsyncContext context = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new MyReadListener(input, context));

  setXXXListner方法指出採用非阻塞I/O而不是傳統I/O。onReadListener可以通過ServletInputStream進行註冊,同樣地,oneWritelistener可以通過ServletOutputStream進行註冊。特別地,新增加的ServletInputStream.isReady方法和ServletInputStream.isFinished方法用於檢測非阻塞I/O的讀取狀態,而ServletOutputStream.canWrite方法用於檢測資料是否能夠無阻塞地寫入。


2)、Servlet 3.1 使用非阻塞IO例項

(1) 請求提交表單 form.html:

<html>
<head>
    <meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>  </title>
</head>
<body>
<form action="asyn" method="post">
    使用者名稱:<input type="text" name="name"/><br/>
    密碼:<input type="text" name="pass"/><br/>
    <input type="submit" value="提交">
    <input type="reset" value="重設">
</form>
</body>
</html>

(2) 在 Servlet中使用非阻塞IO:

@WebServlet(urlPatterns = "/async",asyncSupported=true )
public class AsyncServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        request.setAttribute("param1", "我在非同步處理前被設定...");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println("<title>非阻塞IO示例</title>");
        out.println("進入Servlet的時間:" + new java.util.Date() + ".<br/>");

        //建立AsyncContext物件,開始非同步呼叫
        final AsyncContext async = request.startAsync();

        // 設定非同步呼叫的請求時長
        async.setTimeout(10 * 1000);
        final ServletInputStream input = request.getInputStream();

        // 為輸入流注冊監聽器
        input.setReadListener(new ReadListener() {

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onDataAvailable() throws IOException {
                System.out.println("資料可用!!");
                try
                {
                    // 暫停5秒,模擬讀取資料是一個耗時操作。
                    Thread.sleep(5000);
                    StringBuilder sb = new StringBuilder();
                    int len = -1;
                    byte[] buff = new byte[1024];
                    // 採用原始IO方式讀取瀏覽器向Servlet提交的資料
                    while (input.isReady() && (len = input.read(buff)) > 0)
                    {
                        String data = new String(buff , 0 , len);
                        sb.append(data);
                    }
                    System.out.println(sb);
                    // 將資料設定為request範圍的屬性
                    async.getRequest().setAttribute("data" , sb.toString());
                    // 轉發到檢視頁面
                    async.dispatch("/asyn.jsp");
                }
                catch (Exception ex)
                {
                    ex.printStackTrace();
                }
            }

            @Override
            public void onAllDataRead() throws IOException {
                System.out.println("資料讀取完成");
            }
        });

        out.println("結束Servlet的時間:" + new java.util.Date() + ".<br/>");
        out.flush();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doGet(req, resp);
    }
}

(3) 表現層:

<%@ page contentType="text/html; charset=utf-8" language="java" session="false"%>
<div style="background-color:#ffffdd;height:80px;">
瀏覽器提交資料為:${data}<br/>
<%=new java.util.Date()%>
</div>

(4) 結果展示:

              nio.png-14.3kB

  更多關於Servlet使用、實踐方面的介紹以及Servlet新特性的總結見我的下一篇博文《Servlet 綜述(實踐篇)》。


五、Servlet 3.0 支援檔案上傳

  Servlet 3.0之前的版本中,檔案上傳是個挺讓人頭疼的問題,雖然有第三方框架(Apache Commons)來實現,但使用起來還是比較麻煩。在Servlet 3.0中,這些問題將不復存在,Servlet 3.0對檔案上傳提供了直接支援,配合Servlet 3.0中基於Annotations的配置,大大簡化上傳件的操作。

  在使用表單上傳檔案時,我們需要使用@MultipartConfig註解去修飾對應的Servlet。此外,我們一方面需要在表單裡使用<input type=”file” …/>檔案域,另一方面必須要為表單域設定 enctype 屬性,其有三個值:

  • application/x-www-form-urlencoded:表單資料被編碼為名稱/值對,這是預設的編碼方式;
  • multipart/form-data:以二進位制流的方式處理表單資料,一般用於傳輸二進位制檔案,如圖片、視訊等;
  • text/plain:不編碼特殊字元,適用於通過表單傳送郵件。

下面跟進這個例子來體會其給我們帶來的便捷。

(1) 檔案提交表單:

<%@ page contentType="text/html; charset=utf-8" language="java" errorPage="" %>
<head>
    <title> 檔案上傳 </title>
</head>
<body>
<form method="post" action="upload"  enctype="multipart/form-data">
    檔名:<input type="text" id="name" name="name" /><br/>
    選擇檔案:<input type="file" id="file" name="file" /><br/>
    <input type="submit" value="上傳" /><br/>
</form>
</body>
</html>

(2) 檔案上傳Servlet:

@WebServlet(name="upload" , urlPatterns={"/upload"})
@MultipartConfig
public class UploadServlet extends HttpServlet
{
    public void service(HttpServletRequest request ,
        HttpServletResponse response)
        throws IOException , ServletException
    {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        request.setCharacterEncoding("utf-8");
        // 獲取普通請求引數
        String name = request.getParameter("name");
        out.println("普通的name引數為:" + name + "<br/>");
        // 獲取檔案上傳域
        Part part = request.getPart("file");
        // 獲取上傳檔案的檔案型別
        out.println("上傳檔案的的型別為:"
            + part.getContentType() + "<br/>");
        //獲取上傳檔案的大小。
        out.println("上傳檔案的的大小為:" + part.getSize()  + "<br/>");
        // 獲取該檔案上傳域的Header Name
        Collection<String> headerNames = part.getHeaderNames();
        // 遍歷檔案上傳域的Header Name、Value
        for (String headerName : headerNames)
        {
            out.println(headerName + "--->"
                + part.getHeader(headerName) + "<br/>");
        }
        // 獲取包含原始檔名的字串
        String fileNameInfo = part.getHeader("content-disposition");
        // 提取上傳檔案的原始檔名
        String fileName = fileNameInfo.substring(
            fileNameInfo.indexOf("filename=\"") + 10 , fileNameInfo.length() - 1);
        // 將上傳的檔案寫入伺服器
        part.write(getServletContext().getRealPath("/uploadFiles")
            + "/" + fileName );               // ①

        System.out.println(getServletContext().getRealPath("/uploadFiles"));
    }

(3) 結果展示:

            fileupload.png-11kB


六、拾遺增補

  除上面提到的內容,Servlet 還引入了其他新的特性(如下所述),此不贅述。

  • Servlet 3.0 為 Web 模組化提供了支援;
  • Servlet 3.1 可以強制更改 Session ID,具體由 HttpServletRequest 的 changeSessionId()方法完成。

引用

《輕量級 JavaEE 企業應用實戰(第四版)》
Servlet3.0: 簡介AsyncContext
使用 Servlet 3.1 的非堵塞 I/O 實現可伸縮的應用

相關文章