Linux中國

一個用 Java 實現的超輕量級 RESTful Web 服務示例

Web 服務,以這樣或那樣的形式,已經存在了近二十年。比如,XML-RPC 服務出現在 90 年代後期,緊接著是用 SOAP 分支編寫的服務。在 XML-RPC 和 SOAP 這兩個開拓者之後出現後不久,REST 架構風格的服務在大約 20 年前也出現了。REST 風格(以下簡稱 Restful)服務現在主導了流行的網站,比如 eBay、Facebook 和 Twitter。儘管分散式計算的 Web 服務有很多替代品(如 Web 套接字、微服務和遠程過程調用的新框架),但基於 Restful 的 Web 服務依然具有吸引力,原因如下:

  • Restful 服務建立在現有的基礎設施和協議上,特別是 Web 伺服器和 HTTP/HTTPS 協議。一個擁有基於 HTML 的網站的組織可以很容易地為客戶添加 Web 服務,這些客戶對數據和底層功能更感興趣,而不是對 HTML 的表現形式感興趣。比如,亞馬遜就率先通過網站和 Web 服務(基於 SOAP 或 Restful)提供相同的信息和功能。
  • Restful 服務將 HTTP 當作 API,因此避免了複雜的軟體分層,這種分層是基於 SOAP 的 Web 服務的明顯特徵。比如,Restful API 支持通過 HTTP 命令(POST-GET-PUT-DELETE)進行標準的 CRUD(增加-讀取-更新-刪除)操作;通過 HTTP 狀態碼可以知道請求是否成功或者為什麼失敗。
  • Restful Web 服務可以根據需要變得簡單或複雜。Restful 是一種風格,實際上是一種非常靈活的風格,而不是一套關於如何設計和構造服務的規定。(伴隨而來的缺點是,可能很難確定哪些服務不能算作 Restful 服務。)
  • 作為使用者或者客戶端,Restful Web 服務與語言和平台無關。客戶端發送 HTTP(S) 請求,並以適合現代數據交換的格式(如 JSON)接收文本響應。
  • 幾乎每一種通用編程語言都至少對 HTTP/HTTPS 有足夠的(通常是強大的)支持,這意味著 Web 服務的客戶端可以用這些語言來編寫。

這篇文章將通過一段完整的 Java 代碼示例來探討輕量級的 Restful 服務。

基於 Restful 的「小說」 Web 服務

基於 Restful 的「小說」 web 服務包含三個程序員定義的類:

  • Novel 類代表一個小說,只有三個屬性:機器生成的 ID、作者和標題。屬性可以根據實際情況進行擴展,但我還是想讓這個例子看上去更簡單一些。
  • Novels 類包含了用於各種任務的工具類:將一個 Novel 或者它們的列表的純文本編碼轉換成 XML 或者 JSON;支持在小說集合上進行 CRUD 操作;以及從存儲在文件中的數據初始化集合。Novels 類在 Novel 實例和 servlet 之間起中介作用。
  • NovelsServlet 類是從 HttpServlet 中繼承的,HttpServlet 是一段健壯且靈活的代碼,自 90 年代末的早期企業級 Java 就已經存在了。對於客戶端的 CRUD 請求,servlet 可以當作 HTTP 的端點。 servlet 代碼主要用於處理客戶端的請求和生成相應的響應,而將複雜的細節留給 Novels 類中的工具類進行處理。

一些 Java 框架,比如 Jersey(JAX-RS)和 Restlet,就是為 Restful 服務設計的。儘管如此,HttpServlet 本身為完成這些服務提供了輕量、靈活、強大且充分測試過的 API。我會通過下面的「小說」例子來說明。

部署「小說」 Web 服務

當然,部署「小說」 Web 服務需要一個 Web 伺服器。我的選擇是 Tomcat,但是如果該服務託管在 Jetty 或者甚至是 Java 應用伺服器上,那麼這個服務應該至少可以工作(著名的最後一句話!)。在我的網站上有總結了如何安裝 Tomcat 的 README 文件和代碼。還有一個附帶文檔的 Apache Ant 腳本,可以用來構建「小說」服務(或者任何其他服務或網站),並且將它部署在 Tomcat 或相同的服務。

Tomcat 可以從它的官網上下載。當你在本地安裝後,將 TOMCAT_HOME 設置為安裝目錄。有兩個子目錄值得關註:

  • TOMCAT_HOME/bin 目錄包含了類 Unix 系統(startup.shshutdown.sh)和 Windows(startup.batshutdown.bat) 的啟動和停止腳本。Tomcat 作為 Java 應用程序運行。Web 伺服器的 servlet 容器叫做 Catalina。(在 Jetty 中,Web 伺服器和容器的名字一樣。)當 Tomcat 啟動後,在瀏覽器中輸入 http://localhost:8080/可以查看詳細文檔,包括示例。
  • TOMCAT_HOME/webapps 目錄是已部署的 Web 網站和服務的默認目錄。部署網站或 Web 服務的直接方法是複製以 .war 結尾的 JAR 文件(也就是 WAR 文件)到 TOMCAT_HOME/webapps 或它的子目錄下。然後 Tomcat 會將 WAR 文件解壓到它自己的目錄下。比如,Tomcat 會將 novels.war 文件解壓到一個叫做 novels 的子目錄下,並且保留 novels.war 文件。一個網站或 Web 服務可以通過刪除 WAR 文件進行移除,也可以用一個新版 WAR 文件來覆蓋已有文件進行更新。順便說一下,調試網站或服務的第一步就是檢查 Tomcat 已經正確解壓 WAR 文件;如果沒有的話,網站或服務就無法發布,因為代碼或配置中有致命錯誤。
  • 因為 Tomcat 默認會監聽 8080 埠上的 HTTP 請求,所以本機上的 URL 請求以 http://localhost:8080/ 開始。

通過添加不帶 .war 後綴的 WAR 文件名來訪問由程序員部署的 WAR 文件:

http://locahost:8080/novels/

如果服務部署在 TOMCAT_HOME 下的一個子目錄中(比如,myapps),這會在 URL 中反映出來:

http://locahost:8080/myapps/novels/

我會在靠近文章結尾處的測試部分提供這部分的更多細節。

如前所述,我的主頁上有一個包含 Ant 腳本的 ZIP 文件,這個文件可以編譯並且部署網站或者服務。(這個 ZIP 文件中也包含一個 novels.war 的副本。)對於「小說」這個例子,命令的示例(% 是命令行提示符)如下:

% ant -Dwar.name=novels deploy

這個命令首先會編譯 Java 源代碼,並且創建一個可部署的 novels.war 文件,然後將這個文件保存在當前目錄中,再複製到 TOMCAT_HOME/webapps 目錄中。如果一切順利,GET 請求(使用瀏覽器或者命令行工具,比如 curl)可以用來做一個測試:

% curl http://localhost:8080/novels/

默認情況下,Tomcat 設置為 熱部署 hot deploys :Web 伺服器不需要關閉就可以進行部署、更新或者移除一個 web 應用。

「小說」服務的代碼

讓我們回到「小說」這個例子,不過是在代碼層面。考慮下面的 Novel 類:

例 1:Novel 類

package novels;

import java.io.Serializable;

public class Novel implements Serializable, Comparable<Novel> {
    static final long serialVersionUID = 1L;
    private String author;
    private String title;
    private int id;

    public Novel() { }

    public void setAuthor(final String author) { this.author = author; }
    public String getAuthor() { return this.author; }
    public void setTitle(final String title) { this.title = title; }
    public String getTitle() { return this.title; }
    public void setId(final int id) { this.id = id; }
    public int getId() { return this.id; }

    public int compareTo(final Novel other) { return this.id - other.id; }
}

這個類實現了 Comparable 介面中的 compareTo 方法,因為 Novel 實例是存儲在一個線程安全的無序 ConcurrentHashMap 中。在響應查看集合的請求時,「小說」服務會對從映射中提取的集合(一個 ArrayList)進行排序;compareTo 的實現通過 Novel 的 ID 將它按升序排序。

Novels 類中包含多個實用工具函數:

例 2:Novels 實用工具類

package novels;

import java.io.IOException;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.file.Files;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Collections;
import java.beans.XMLEncoder;
import javax.servlet.ServletContext; // not in JavaSE
import org.json.JSONObject;
import org.json.XML;

public class Novels {
    private final String fileName = "/WEB-INF/data/novels.db";
    private ConcurrentMap<Integer, Novel> novels;
    private ServletContext sctx;
    private AtomicInteger mapKey;

    public Novels() {
        novels = new ConcurrentHashMap<Integer, Novel>();
        mapKey = new AtomicInteger();
    }

    public void setServletContext(ServletContext sctx) { this.sctx = sctx; }
    public ServletContext getServletContext() { return this.sctx; }

    public ConcurrentMap<Integer, Novel> getConcurrentMap() {
        if (getServletContext() == null) return null; // not initialized
        if (novels.size() < 1) populate();
        return this.novels;
    }

    public String toXml(Object obj) { // default encoding
        String xml = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            XMLEncoder encoder = new XMLEncoder(out);
            encoder.writeObject(obj);
            encoder.close();
            xml = out.toString();
        }
        catch(Exception e) { }
        return xml;
    }

    public String toJson(String xml) { // option for requester
        try {
            JSONObject jobt = XML.toJSONObject(xml);
            return jobt.toString(3); // 3 is indentation level
        }
        catch(Exception e) { }
        return null;
    }

    public int addNovel(Novel novel) {
        int id = mapKey.incrementAndGet();
        novel.setId(id);
        novels.put(id, novel);
        return id;
    }

    private void populate() {
        InputStream in = sctx.getResourceAsStream(this.fileName);
        // Convert novel.db string data into novels.
        if (in != null) {
            try {
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader reader = new BufferedReader(isr);

                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    if (parts.length == 2) {
                        Novel novel = new Novel();
                        novel.setAuthor(parts[0]);
                        novel.setTitle(parts[1]);
                        addNovel(novel); // sets the Id, adds to map
                    }
                }
                in.close();
            }
            catch (IOException e) { }
        }
    }
}

最複雜的方法是 populate,這個方法從一個包含在 WAR 文件中的文本文件讀取。這個文本文件包括了「小說」的初始集合。要打開此文件,populate 方法需要 ServletContext,這是一個 Java 映射類型,包含了關於嵌入在 servlet 容器中的 servlet 的所有關鍵信息。這個文本文件有包含了像下面這樣的記錄:

Jane Austen!Persuasion

這一行被解析為兩部分(作者和標題),由感嘆號(!)分隔。然後這個方法創建一個 Novel 實例,設置作者和標題屬性,並且將「小說」加到容器中,保存在內存中。

Novels 類也有一些實用工具函數,可以將「小說」容器編碼為 XML 或 JSON,取決於發出請求的人所要求的格式。默認是 XML 格式,但是也可以請求 JSON 格式。一個輕量級的 XML 轉 JSON 包提供了 JSON。下面是關於編碼的更多細節。

例 3:NovelsServlet 類

package novels;

import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.beans.XMLEncoder;
import org.json.JSONObject;
import org.json.XML;

public class NovelsServlet extends HttpServlet {
    static final long serialVersionUID = 1L;
    private Novels novels; // back-end bean

    // Executed when servlet is first loaded into container.
    @Override
    public void init() {
        this.novels = new Novels();
        novels.setServletContext(this.getServletContext());
    }

    // GET /novels
    // GET /novels?id=1
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));

        // Check user preference for XML or JSON by inspecting
        // the HTTP headers for the Accept key.
        boolean json = false;
        String accept = request.getHeader("accept");
        if (accept != null && accept.contains("json")) json = true;

        // If no query string, assume client wants the full list.
        if (key == null) {
            ConcurrentMap<Integer, Novel> map = novels.getConcurrentMap();
            Object list = map.values().toArray();
            Arrays.sort(list);

            String payload = novels.toXml(list);        // defaults to Xml
            if (json) payload = novels.toJson(payload); // Json preferred?
            sendResponse(response, payload);
        }
        // Otherwise, return the specified Novel.
        else {
            Novel novel = novels.getConcurrentMap().get(key);
            if (novel == null) { // no such Novel
                String msg = key + " does not map to a novel.n";
                sendResponse(response, novels.toXml(msg));
            }
            else { // requested Novel found
                if (json) sendResponse(response, novels.toJson(novels.toXml(novel)));
                else sendResponse(response, novels.toXml(novel));
            }
        }
    }

    // POST /novels
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String author = request.getParameter("author");
        String title = request.getParameter("title");

        // Are the data to create a new novel present?
        if (author == null || title == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Create a novel.
        Novel n = new Novel();
        n.setAuthor(author);
        n.setTitle(title);

        // Save the ID of the newly created Novel.
        int id = novels.addNovel(n);

        // Generate the confirmation message.
        String msg = "Novel " + id + " created.n";
        sendResponse(response, novels.toXml(msg));
    }

    // PUT /novels
    @Override
    public void doPut(HttpServletRequest request, HttpServletResponse response) {
        /* A workaround is necessary for a PUT request because Tomcat does not
 generate a workable parameter map for the PUT verb. */
        String key = null;
        String rest = null;
        boolean author = false;

        /* Let the hack begin. */
        try {
            BufferedReader br =
                new BufferedReader(new InputStreamReader(request.getInputStream()));
            String data = br.readLine();
            /* To simplify the hack, assume that the PUT request has exactly
 two parameters: the id and either author or title. Assume, further,
 that the id comes first. From the client side, a hash character
 # separates the id and the author/title, e.g.,

 id=33#title=War and Peace
 */
            String[] args = data.split("#");      // id in args[0], rest in args[1]
            String[] parts1 = args[0].split("="); // id = parts1[1]
            key = parts1[1];

            String[] parts2 = args[1].split("="); // parts2[0] is key
            if (parts2[0].contains("author")) author = true;
            rest = parts2[1];
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }

        // If no key, then the request is ill formed.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Look up the specified novel.
        Novel p = novels.getConcurrentMap().get(Integer.valueOf((key.trim())));
        if (p == null) { // not found
            String msg = key + " does not map to a novel.n";
            sendResponse(response, novels.toXml(msg));
        }
        else { // found
            if (rest == null) {
                throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
            }
            // Do the editing.
            else {
                if (author) p.setAuthor(rest);
                else p.setTitle(rest);

                String msg = "Novel " + key + " has been edited.n";
                sendResponse(response, novels.toXml(msg));
            }
        }
    }

    // DELETE /novels?id=1
    @Override
    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));
        // Only one Novel can be deleted at a time.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
        try {
            novels.getConcurrentMap().remove(key);
            String msg = "Novel " + key + " removed.n";
            sendResponse(response, novels.toXml(msg));
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }

    // Methods Not Allowed
    @Override
    public void doTrace(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doHead(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doOptions(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    // Send the response payload (Xml or Json) to the client.
    private void sendResponse(HttpServletResponse response, String payload) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(payload.getBytes());
            out.flush();
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }
}

上面的 NovelsServlet 類繼承了 HttpServlet 類,HttpServlet 類繼承了 GenericServlet 類,後者實現了 Servlet 介面:

NovelsServlet extends HttpServlet extends GenericServlet implements Servlet

從名字可以很清楚的看出來,HttpServlet 是為實現 HTTP(S) 上的 servlet 設計的。這個類提供了以標準 HTTP 請求動詞(官方說法, 方法 methods )命名的空方法:

  • doPost (Post = 創建)
  • doGet (Get = 讀取)
  • doPut (Put = 更新)
  • doDelete (Delete = 刪除)

其他一些 HTTP 動詞也會涉及到。HttpServlet 的子類,比如 NovelsServlet,會重載相關的 do 方法,並且保留其他方法為 no-ops NovelsServlet 重載了七個 do 方法。

每個 HttpServlet 的 CRUD 方法都有兩個相同的參數。下面以 doPost 為例:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 參數是一個 HTTP 請求信息的映射,而 response 提供了一個返回給請求者的輸出流。像 doPost 的方法,結構如下:

  • 讀取 request 信息,採取任何適當的措施生成一個響應。如果該信息丟失或者損壞了,就會生成一個錯誤。
  • 使用提取的請求信息來執行適當的 CRUD 操作(在本例中,創建一個 Novel),然後使用 response 輸出流為請求者編碼一個適當的響應。在 doPost 例子中,響應就是已經成功生成一個新「小說」並且添加到容器中的一個確認。當響應被發送後,輸出流就關閉了,同時也將連接關閉了。

關於方法重載的更多內容

HTTP 請求的格式相對比較簡單。下面是一個非常熟悉的 HTTP 1.1 的格式,注釋由雙井號分隔:

GET /novels              ## start line
Host: localhost:8080     ## header element
Accept-type: text/plain  ## ditto
...
[body]                   ## POST and PUT only

第一行由 HTTP 動詞(在本例中是 GET)和以名詞(在本例中是 novels)命名目標資源的 URI 開始。報頭中包含鍵-值對,用冒號分隔左面的鍵和右面的值。報頭中的鍵 Host(大小寫敏感)是必須的;主機名 localhost 是當前機器上的本地符號地址,8080 埠是 Tomcat web 伺服器上等待 HTTP 請求的默認埠。(默認情況下,Tomcat 在 8443 埠上監聽 HTTP 請求。)報頭元素可以以任意順序出現。在這個例子中,Accept-type 報頭的值是 MIME 類型 text/plain

一些請求(特別是 POSTPUT)會有報文,而其他請求(特別是 GETDELETE)沒有。如果有報文(可能為空),以兩個換行符將報頭和報文分隔開;HTTP 報文包含一系列鍵-值對。對於無報文的請求,比如說查詢字元串,報頭元素就可以用來發送信息。下面是一個用 ID 2 對 /novels 資源的 GET 請求:

GET /novels?id=2

通常來說,查詢字元串以問號開始,並且包含一個鍵-值對,儘管這個鍵-值可能值為空。

帶有 getParametergetParameterMap 等方法的 HttpServlet 很好地迴避了有報文和沒有報文的 HTTP 請求之前的差異。在「小說」例子中,getParameter 方法用來從 GETPOSTDELETE 方法中提取所需的信息。(處理 PUT請求需要更底層的代碼,因為 Tomcat 沒有提供可以解析 PUT 請求的參數映射。)下面展示了一段在 NovelsServlet中被重載的 doPost 方法:

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
   String author = request.getParameter("author");
   String title = request.getParameter("title");
   ...

對於沒有報文的 DELETE 請求,過程基本是一樣的:

@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
   String param = request.getParameter("id"); // id of novel to be removed
   ...

doGet 方法需要區分 GET 請求的兩種方式:一種是「獲得所有」,而另一種是「獲得某一個」。如果 GET 請求 URL 中包含一個鍵是一個 ID 的查詢字元串,那麼這個請求就被解析為「獲得某一個」:

http://localhost:8080/novels?id=2  ## GET specified

如果沒有查詢字元串,GET 請求就會被解析為「獲得所有」:

http://localhost:8080/novels       ## GET all

一些值得注意的細節

「小說」服務的設計反映了像 Tomcat 這樣基於 Java 的 web 伺服器是如何工作的。在啟動時,Tomcat 構建一個線程池,從中提取請求處理程序,這種方法稱為 「 每個請求一個線程 one thread per request 」 模型。現在版本的 Tomcat 使用非阻塞 I/O 來提高個性能。

「小說」服務是作為 NovelsServlet 類的單個實例來執行的,該實例也就維護了一個「小說」集合。相應的,也就會出現競態條件,比如出現兩個請求同時被處理:

  • 一個請求向集合中添加一個新「小說」。
  • 另一個請求獲得集合中的所有「小說」。

這樣的結果是不確定的,取決與 的操作是以怎樣的順序進行操作的。為了避免這個問題,「小說」服務使用了線程安全的 ConcurrentMap。這個映射的關鍵是生成了一個線程安全的 AtomicInteger。下面是相關的代碼片段:

public class Novels {
    private ConcurrentMap<Integer, Novel> novels;
    private AtomicInteger mapKey;
    ...

默認情況下,對客戶端請求的響應被編碼為 XML。為了簡單,「小說」程序使用了以前的 XMLEncoder 類;另一個包含更豐富功能的方式是使用 JAX-B 庫。代碼很簡單:

public String toXml(Object obj) { // default encoding
   String xml = null;
   try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      XMLEncoder encoder = new XMLEncoder(out);
      encoder.writeObject(obj);
      encoder.close();
      xml = out.toString();
   }
   catch(Exception e) { }
   return xml;
}

Object 參數要麼是一個有序的「小說」 ArraList(用以響應「 獲得所有 get all 」請求),要麼是一個 Novel 實例(用以響應「 獲得一個 get one 」請求),又或者是一個 String(確認消息)。

如果 HTTP 請求報頭指定 JSON 作為所需要的類型,那麼 XML 就被轉化成 JSON。下面是 NovelsServlet 中的 doGet 方法中的檢查:

String accept = request.getHeader("accept"); // "accept" is case insensitive
if (accept != null && accept.contains("json")) json = true;

Novels類中包含了 toJson 方法,可以將 XML 轉換成 JSON:

public String toJson(String xml) { // option for requester
   try {
      JSONObject jobt = XML.toJSONObject(xml);
      return jobt.toString(3); // 3 is indentation level
   }
   catch(Exception e) { }
   return null;
}

NovelsServlet會對各種類型進行錯誤檢查。比如,POST 請求應該包含新「小說」的作者和標題。如果有一個丟了,doPost 方法會拋出一個異常:

if (author == null || title == null)
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

SC_BAD_REQUEST 中的 SC 代表的是 狀態碼 status code BAD_REQUEST 的標準 HTTP 數值是 400。如果請求中的 HTTP 動詞是 TRACE,會返回一個不同的狀態碼:

public void doTrace(HttpServletRequest request, HttpServletResponse response) {
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
}

測試「小說」服務

用瀏覽器測試 web 服務會很不順手。在 CRUD 動詞中,現代瀏覽器只能生成 POST(創建)和 GET(讀取)請求。甚至從瀏覽器發送一個 POST 請求都有點不好辦,因為報文需要包含鍵-值對;這樣的測試通常通過 HTML 表單完成。命令行工具,比如說 curl,是一個更好的選擇,這個部分展示的一些 curl 命令,已經包含在我網站的 ZIP 文件中了。

下面是一些測試樣例,沒有展示相應的輸出結果:

% curl localhost:8080/novels/
% curl localhost:8080/novels?id=1
% curl --header "Accept: application/json" localhost:8080/novels/

第一條命令請求所有「小說」,默認是 XML 編碼。第二條命令請求 ID 為 1 的「小說」,XML 編碼。最後一條命令通過 application/json 添加了 Accept 報頭元素,作為所需要的 MIME 類型。「 獲得一個 get one 」命令也可以用這個報頭。這些請求用了 JSON 而不是 XML 編碼作為響應。

下面兩條命令在集合中創建了一個新「小說」,並且確認添加了進去:

% curl --request POST --data "author=Tolstoy&title=War and Peace" localhost:8080/novels/
% curl localhost:8080/novels?id=4

curl 中的 PUT 命令與 POST 命令相似,不同的地方是 PUT 的報文沒有使用標準的語法。在 NovelsServlet 中關於 doPut 方法的文檔中有詳細的介紹,但是簡單來說,Tomcat 不會對 PUT 請求生成合適的映射。下面是一個 PUT 命令和確認命令的的例子:

% curl --request PUT --data "id=3#title=This is an UPDATE" localhost:8080/novels/
% curl localhost:8080/novels?id=3

第二個命令確認了集合已經更新。

最後,DELETE 命令會正常運行:

% curl --request DELETE localhost:8080/novels?id=2
% curl localhost:8080/novels/

這個請求是刪除 ID 為 2 的「小說」。第二個命令會顯示剩餘的「小說」。

web.xml 配置文件

儘管官方規定它是可選的,web.xml 配置文件是一個生產級別網站或服務的重要組成部分。這個配置文件可以配置獨立於代碼的路由、安全性,或者網站或服務的其他功能。「小說」服務的配置通過為該服務的請求分配一個 URL 模式來配置路由:

<xml version = "1.0" encoding = "UTF-8">
<web-app>
   <servlet>
     <servlet-name>novels</servlet-name>
     <servlet-class>novels.NovelsServlet</servlet-class>
   </servlet>
   <servlet-mapping>
     <servlet-name>novels</servlet-name>
     <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

servlet-name 元素為 servlet 全名(novels.NovelsServlet)提供了一個縮寫(novels),然後這個名字在下面的 servlet-mapping 元素中使用。

回想一下,一個已部署服務的 URL 會在埠號後面有 WAR 文件的文件名:

http://localhost:8080/novels/

埠號後斜杠後的 URI,是所請求資源的「路徑」,在這個例子中,就是「小說」服務。因此,novels 出現在了第一個單斜杠後。

web.xml 文件中,url-patter 被指定為 /*,代表 「以 /novels 為起始的任意路徑」。假設 Tomcat 遇到了一個不存在的 URL,像這樣:

http://localhost:8080/novels/foobar/

web.xml 配置也會指定這個請求被分配到「小說」 servlet 中,因為 /* 模式也包含 /foobar。因此,這個不存在的 URL 也會得到像上面合法路徑的相同結果。

生產級別的配置文件可能會包含安全相關的信息,包括 連接級別 wire-level 用戶角色 users-roles 。即使在這種情況下,配置文件的大小也只會是這個例子中的兩到三倍大。

總結

HttpServlet 是 Java web 技術的核心。像「小說」這樣的網站或 web 服務繼承了這個類,並且根據需求重載了相應的 do 動詞方法。像 Jersay(JAX-RS)或 Restlet 這樣的 Restful 框架通過提供定製的 servlet 完成了基本相同的功能,針對框架中的 web 應用程序的請求,這個 servlet 扮演著 HTTP(S) 終端 endpoint 的角色。

當然,基於 servlet 的應用程序可以訪問 web 應用程序中所需要的任何 Java 庫。如果應用程序遵循 關注點分離 separation-of-concerns 原則,那麼 servlet 代碼仍然相當簡單:代碼會檢查請求,如果存在缺陷,就會發出適當的錯誤;否則,代碼會調用所需要的功能(比如,查詢資料庫,以特定格式為響應編碼),然後向請求這發送響應。HttpServletRequestHttpServletReponse 類型使得讀取請求和編寫響應變得簡單。

Java 的 API 可以從非常簡單變得相當複雜。如果你需要用 Java 交付一些 Restful 服務的話,我的建議是在做其他事情之前先嘗試一下簡單的 HttpServlet

via: https://opensource.com/article/20/7/restful-services-java

作者:Marty Kalin 選題:lujun9972 譯者:Yufei-Yan 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國