日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

一個(gè)用Java實(shí)現(xiàn)的超輕量級(jí)RESTful Web服務(wù)示例

 

通過(guò)管理一套圖書(shū)的完整代碼示例,來(lái)探索輕量級(jí)的 RESTful 服務(wù)。

• 來(lái)源:linux.cn • 作者:Marty Kalin • 譯者:MCGA •

(本文字?jǐn)?shù):24337,閱讀時(shí)長(zhǎng)大約:28 分鐘)

Web 服務(wù),以這樣或那樣的形式,已經(jīng)存在了近二十年。比如, XML-RPC 服務(wù) 出現(xiàn)在 90 年代后期,緊接著是用 SOAP 分支編寫(xiě)的服務(wù)。在 XML-RPC 和 SOAP 這兩個(gè)開(kāi)拓者之后出現(xiàn)后不久,REST 架構(gòu)風(fēng)格的服務(wù)在大約 20 年前也出現(xiàn)了。 REST 風(fēng)格(以下簡(jiǎn)稱 Restful)服務(wù)現(xiàn)在主導(dǎo)了流行的網(wǎng)站,比如 eBay、Facebook 和 Twitter。盡管分布式計(jì)算的 Web 服務(wù)有很多替代品(如 Web 套接字、微服務(wù)和遠(yuǎn)程過(guò)程調(diào)用的新框架),但基于 Restful 的 Web 服務(wù)依然具有吸引力,原因如下:

  • Restful 服務(wù)建立在現(xiàn)有的基礎(chǔ)設(shè)施和協(xié)議上,特別是 Web 服務(wù)器和 HTTP/HTTPS 協(xié)議。一個(gè)擁有基于 html 的網(wǎng)站的組織可以很容易地為客戶添加 Web 服務(wù),這些客戶對(duì)數(shù)據(jù)和底層功能更感興趣,而不是對(duì) HTML 的表現(xiàn)形式感興趣。比如,亞馬遜就率先通過(guò)網(wǎng)站和 Web 服務(wù)(基于 SOAP 或 Restful)提供相同的信息和功能。
  • Restful 服務(wù)將 HTTP 當(dāng)作 API,因此避免了復(fù)雜的軟件分層,這種分層是基于 SOAP 的 Web 服務(wù)的明顯特征。比如,Restful API 支持通過(guò) HTTP 命令(POST-GET-PUT-DELETE)進(jìn)行標(biāo)準(zhǔn)的 CRUD(增加-讀取-更新-刪除)操作;通過(guò) HTTP 狀態(tài)碼可以知道請(qǐng)求是否成功或者為什么失敗。
  • Restful Web 服務(wù)可以根據(jù)需要變得簡(jiǎn)單或復(fù)雜。Restful 是一種風(fēng)格,實(shí)際上是一種非常靈活的風(fēng)格,而不是一套關(guān)于如何設(shè)計(jì)和構(gòu)造服務(wù)的規(guī)定。(伴隨而來(lái)的缺點(diǎn)是,可能很難確定哪些服務(wù)不能算作 Restful 服務(wù)。)
  • 作為使用者或者客戶端,Restful Web 服務(wù)與語(yǔ)言和平臺(tái)無(wú)關(guān)。客戶端發(fā)送 HTTP(S) 請(qǐng)求,并以適合現(xiàn)代數(shù)據(jù)交換的格式(如 JSON)接收文本響應(yīng)。
  • 幾乎每一種通用編程語(yǔ)言都至少對(duì) HTTP/HTTPS 有足夠的(通常是強(qiáng)大的)支持,這意味著 Web 服務(wù)的客戶端可以用這些語(yǔ)言來(lái)編寫(xiě)。

這篇文章將通過(guò)一段完整的 JAVA 代碼示例來(lái)探討輕量級(jí)的 Restful 服務(wù)。

基于 Restful 的“小說(shuō)” Web 服務(wù)

基于 Restful 的“小說(shuō)” web 服務(wù)包含三個(gè)程序員定義的類(lèi):

  • Novel 類(lèi)代表一個(gè)小說(shuō),只有三個(gè)屬性:機(jī)器生成的 ID、作者和標(biāo)題。屬性可以根據(jù)實(shí)際情況進(jìn)行擴(kuò)展,但我還是想讓這個(gè)例子看上去更簡(jiǎn)單一些。
  • Novels 類(lèi)包含了用于各種任務(wù)的工具類(lèi):將一個(gè) Novel 或者它們的列表的純文本編碼轉(zhuǎn)換成 XML 或者 JSON;支持在小說(shuō)集合上進(jìn)行 CRUD 操作;以及從存儲(chǔ)在文件中的數(shù)據(jù)初始化集合。Novels 類(lèi)在 Novel 實(shí)例和 servlet 之間起中介作用。
  • NovelsServlet 類(lèi)是從 HttpServlet 中繼承的,HttpServlet 是一段健壯且靈活的代碼,自 90 年代末的早期企業(yè)級(jí) Java 就已經(jīng)存在了。對(duì)于客戶端的 CRUD 請(qǐng)求,servlet 可以當(dāng)作 HTTP 的端點(diǎn)。 servlet 代碼主要用于處理客戶端的請(qǐng)求和生成相應(yīng)的響應(yīng),而將復(fù)雜的細(xì)節(jié)留給 Novels 類(lèi)中的工具類(lèi)進(jìn)行處理。

一些 Java 框架,比如 Jersey(JAX-RS)和 Restlet,就是為 Restful 服務(wù)設(shè)計(jì)的。盡管如此,HttpServlet 本身為完成這些服務(wù)提供了輕量、靈活、強(qiáng)大且充分測(cè)試過(guò)的 API。我會(huì)通過(guò)下面的“小說(shuō)”例子來(lái)說(shuō)明。

部署“小說(shuō)” Web 服務(wù)

當(dāng)然,部署“小說(shuō)” Web 服務(wù)需要一個(gè) Web 服務(wù)器。我的選擇是 Tomcat ,但是如果該服務(wù)托管在 Jetty 或者甚至是 Java 應(yīng)用服務(wù)器上,那么這個(gè)服務(wù)應(yīng)該至少可以工作(著名的最后一句話?。?在我的網(wǎng)站上 有總結(jié)了如何安裝 Tomcat 的 README 文件和代碼。還有一個(gè)附帶文檔的 Apache Ant 腳本,可以用來(lái)構(gòu)建“小說(shuō)”服務(wù)(或者任何其他服務(wù)或網(wǎng)站),并且將它部署在 Tomcat 或相同的服務(wù)。

Tomcat 可以從它的 官網(wǎng) 上下載。當(dāng)你在本地安裝后,將 TOMCAT_HOME 設(shè)置為安裝目錄。有兩個(gè)子目錄值得關(guān)注:

  • TOMCAT_HOME/bin 目錄包含了類(lèi) Unix 系統(tǒng)(startup.sh 和 shutdown.sh)和 windows(startup.bat 和 shutdown.bat) 的啟動(dòng)和停止腳本。Tomcat 作為 Java 應(yīng)用程序運(yùn)行。Web 服務(wù)器的 servlet 容器叫做 Catalina。(在 Jetty 中,Web 服務(wù)器和容器的名字一樣。)當(dāng) Tomcat 啟動(dòng)后,在瀏覽器中輸入 http://localhost:8080/可以查看詳細(xì)文檔,包括示例。
  • TOMCAT_HOME/webApps 目錄是已部署的 Web 網(wǎng)站和服務(wù)的默認(rèn)目錄。部署網(wǎng)站或 Web 服務(wù)的直接方法是復(fù)制以 .war 結(jié)尾的 JAR 文件(也就是 WAR 文件)到 TOMCAT_HOME/webapps 或它的子目錄下。然后 Tomcat 會(huì)將 WAR 文件解壓到它自己的目錄下。比如,Tomcat 會(huì)將 novels.war 文件解壓到一個(gè)叫做 novels 的子目錄下,并且保留 novels.war 文件。一個(gè)網(wǎng)站或 Web 服務(wù)可以通過(guò)刪除 WAR 文件進(jìn)行移除,也可以用一個(gè)新版 WAR 文件來(lái)覆蓋已有文件進(jìn)行更新。順便說(shuō)一下,調(diào)試網(wǎng)站或服務(wù)的第一步就是檢查 Tomcat 已經(jīng)正確解壓 WAR 文件;如果沒(méi)有的話,網(wǎng)站或服務(wù)就無(wú)法發(fā)布,因?yàn)榇a或配置中有致命錯(cuò)誤。
  • 因?yàn)?Tomcat 默認(rèn)會(huì)監(jiān)聽(tīng) 8080 端口上的 HTTP 請(qǐng)求,所以本機(jī)上的 URL 請(qǐng)求以 http://localhost:8080/ 開(kāi)始。

通過(guò)添加不帶 .war 后綴的 WAR 文件名來(lái)訪問(wèn)由程序員部署的 WAR 文件:

http://locahost:8080/novels/

如果服務(wù)部署在 TOMCAT_HOME 下的一個(gè)子目錄中(比如,myapps),這會(huì)在 URL 中反映出來(lái):

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

我會(huì)在靠近文章結(jié)尾處的測(cè)試部分提供這部分的更多細(xì)節(jié)。

如前所述,我的主頁(yè)上有一個(gè)包含 Ant 腳本的 ZIP 文件,這個(gè)文件可以編譯并且部署網(wǎng)站或者服務(wù)。(這個(gè) ZIP 文件中也包含一個(gè) novels.war 的副本。)對(duì)于“小說(shuō)”這個(gè)例子,命令的示例(% 是命令行提示符)如下:

% ant -Dwar.name=novels deploy

這個(gè)命令首先會(huì)編譯 Java 源代碼,并且創(chuàng)建一個(gè)可部署的 novels.war 文件,然后將這個(gè)文件保存在當(dāng)前目錄中,再?gòu)?fù)制到 TOMCAT_HOME/webapps 目錄中。如果一切順利,GET 請(qǐng)求(使用瀏覽器或者命令行工具,比如 curl)可以用來(lái)做一個(gè)測(cè)試:

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

默認(rèn)情況下,Tomcat 設(shè)置為 熱部署(hot deploys):Web 服務(wù)器不需要關(guān)閉就可以進(jìn)行部署、更新或者移除一個(gè) web 應(yīng)用。

“小說(shuō)”服務(wù)的代碼

讓我們回到“小說(shuō)”這個(gè)例子,不過(guò)是在代碼層面。考慮下面的 Novel 類(lèi):

例 1:Novel 類(lèi)

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; }
}

這個(gè)類(lèi)實(shí)現(xiàn)了 Comparable 接口中的 compareTo 方法,因?yàn)?Novel 實(shí)例是存儲(chǔ)在一個(gè)線程安全的無(wú)序 ConcurrentHashMap 中。在響應(yīng)查看集合的請(qǐng)求時(shí),“小說(shuō)”服務(wù)會(huì)對(duì)從映射中提取的集合(一個(gè) ArrayList)進(jìn)行排序;compareTo 的實(shí)現(xiàn)通過(guò) Novel 的 ID 將它按升序排序。

Novels 類(lèi)中包含多個(gè)實(shí)用工具函數(shù):

例 2:Novels 實(shí)用工具類(lèi)

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) { }
        }
    }
}

最復(fù)雜的方法是 populate,這個(gè)方法從一個(gè)包含在 WAR 文件中的文本文件讀取。這個(gè)文本文件包括了“小說(shuō)”的初始集合。要打開(kāi)此文件,populate 方法需要 ServletContext,這是一個(gè) Java 映射類(lèi)型,包含了關(guān)于嵌入在 servlet 容器中的 servlet 的所有關(guān)鍵信息。這個(gè)文本文件有包含了像下面這樣的記錄:

Jane Austen!Persuasion

這一行被解析為兩部分(作者和標(biāo)題),由感嘆號(hào)(!)分隔。然后這個(gè)方法創(chuàng)建一個(gè) Novel 實(shí)例,設(shè)置作者和標(biāo)題屬性,并且將“小說(shuō)”加到容器中,保存在內(nèi)存中。

Novels 類(lèi)也有一些實(shí)用工具函數(shù),可以將“小說(shuō)”容器編碼為 XML 或 JSON,取決于發(fā)出請(qǐng)求的人所要求的格式。默認(rèn)是 XML 格式,但是也可以請(qǐng)求 JSON 格式。一個(gè)輕量級(jí)的 XML 轉(zhuǎn) JSON 包提供了 JSON。下面是關(guān)于編碼的更多細(xì)節(jié)。

例 3:NovelsServlet 類(lèi)

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 類(lèi)繼承了 HttpServlet 類(lèi),HttpServlet 類(lèi)繼承了 GenericServlet 類(lèi),后者實(shí)現(xiàn)了 Servlet 接口:

NovelsServlet extends HttpServlet extends GenericServlet implements Servlet

從名字可以很清楚的看出來(lái),HttpServlet 是為實(shí)現(xiàn) HTTP(S) 上的 servlet 設(shè)計(jì)的。這個(gè)類(lèi)提供了以標(biāo)準(zhǔn) HTTP 請(qǐng)求動(dòng)詞(官方說(shuō)法, 方法(methods))命名的空方法:

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

其他一些 HTTP 動(dòng)詞也會(huì)涉及到。HttpServlet 的子類(lèi),比如 NovelsServlet,會(huì)重載相關(guān)的 do 方法,并且保留其他方法為 空(no-ops)。NovelsServlet 重載了七個(gè) do 方法。

每個(gè) HttpServlet 的 CRUD 方法都有兩個(gè)相同的參數(shù)。下面以 doPost 為例:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 參數(shù)是一個(gè) HTTP 請(qǐng)求信息的映射,而 response 提供了一個(gè)返回給請(qǐng)求者的輸出流。像 doPost 的方法,結(jié)構(gòu)如下:

  • 讀取 request 信息,采取任何適當(dāng)?shù)拇胧┥梢粋€(gè)響應(yīng)。如果該信息丟失或者損壞了,就會(huì)生成一個(gè)錯(cuò)誤。
  • 使用提取的請(qǐng)求信息來(lái)執(zhí)行適當(dāng)?shù)?CRUD 操作(在本例中,創(chuàng)建一個(gè) Novel),然后使用 response 輸出流為請(qǐng)求者編碼一個(gè)適當(dāng)?shù)捻憫?yīng)。在 doPost 例子中,響應(yīng)就是已經(jīng)成功生成一個(gè)新“小說(shuō)”并且添加到容器中的一個(gè)確認(rèn)。當(dāng)響應(yīng)被發(fā)送后,輸出流就關(guān)閉了,同時(shí)也將連接關(guān)閉了。

關(guān)于方法重載的更多內(nèi)容

HTTP 請(qǐng)求的格式相對(duì)比較簡(jiǎn)單。下面是一個(gè)非常熟悉的 HTTP 1.1 的格式,注釋由雙井號(hào)分隔:

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

第一行由 HTTP 動(dòng)詞(在本例中是 GET)和以名詞(在本例中是 novels)命名目標(biāo)資源的 URI 開(kāi)始。報(bào)頭中包含鍵-值對(duì),用冒號(hào)分隔左面的鍵和右面的值。報(bào)頭中的鍵 Host(大小寫(xiě)敏感)是必須的;主機(jī)名 localhost 是當(dāng)前機(jī)器上的本地符號(hào)地址,8080 端口是 Tomcat web 服務(wù)器上等待 HTTP 請(qǐng)求的默認(rèn)端口。(默認(rèn)情況下,Tomcat 在 8443 端口上監(jiān)聽(tīng) HTTP 請(qǐng)求。)報(bào)頭元素可以以任意順序出現(xiàn)。在這個(gè)例子中,Accept-type 報(bào)頭的值是 MIME 類(lèi)型 text/plain。

一些請(qǐng)求(特別是 POST 和 PUT)會(huì)有報(bào)文,而其他請(qǐng)求(特別是 GET 和 DELETE)沒(méi)有。如果有報(bào)文(可能為空),以兩個(gè)換行符將報(bào)頭和報(bào)文分隔開(kāi);HTTP 報(bào)文包含一系列鍵-值對(duì)。對(duì)于無(wú)報(bào)文的請(qǐng)求,比如說(shuō)查詢字符串,報(bào)頭元素就可以用來(lái)發(fā)送信息。下面是一個(gè)用 ID 2 對(duì) /novels 資源的 GET 請(qǐng)求:

GET /novels?id=2

通常來(lái)說(shuō),查詢字符串以問(wèn)號(hào)開(kāi)始,并且包含一個(gè)鍵-值對(duì),盡管這個(gè)鍵-值可能值為空。

帶有 getParameter 和 getParameterMap 等方法的 HttpServlet 很好地回避了有報(bào)文和沒(méi)有報(bào)文的 HTTP 請(qǐng)求之前的差異。在“小說(shuō)”例子中,getParameter 方法用來(lái)從 GET、POST 和 DELETE 方法中提取所需的信息。(處理 PUT請(qǐng)求需要更底層的代碼,因?yàn)?Tomcat 沒(méi)有提供可以解析 PUT 請(qǐng)求的參數(shù)映射。)下面展示了一段在 NovelsServlet中被重載的 doPost 方法:

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

對(duì)于沒(méi)有報(bào)文的 DELETE 請(qǐng)求,過(guò)程基本是一樣的:

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

doGet 方法需要區(qū)分 GET 請(qǐng)求的兩種方式:一種是“獲得所有”,而另一種是“獲得某一個(gè)”。如果 GET 請(qǐng)求 URL 中包含一個(gè)鍵是一個(gè) ID 的查詢字符串,那么這個(gè)請(qǐng)求就被解析為“獲得某一個(gè)”:

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

如果沒(méi)有查詢字符串,GET 請(qǐng)求就會(huì)被解析為“獲得所有”:

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

一些值得注意的細(xì)節(jié)

“小說(shuō)”服務(wù)的設(shè)計(jì)反映了像 Tomcat 這樣基于 Java 的 web 服務(wù)器是如何工作的。在啟動(dòng)時(shí),Tomcat 構(gòu)建一個(gè)線程池,從中提取請(qǐng)求處理程序,這種方法稱為 “ 每個(gè)請(qǐng)求一個(gè)線程(one thread per request)” 模型。現(xiàn)在版本的 Tomcat 使用非阻塞 I/O 來(lái)提高個(gè)性能。

“小說(shuō)”服務(wù)是作為 NovelsServlet 類(lèi)的單個(gè)實(shí)例來(lái)執(zhí)行的,該實(shí)例也就維護(hù)了一個(gè)“小說(shuō)”集合。相應(yīng)的,也就會(huì)出現(xiàn)競(jìng)態(tài)條件,比如出現(xiàn)兩個(gè)請(qǐng)求同時(shí)被處理:

  • 一個(gè)請(qǐng)求向集合中添加一個(gè)新“小說(shuō)”。
  • 另一個(gè)請(qǐng)求獲得集合中的所有“小說(shuō)”。

這樣的結(jié)果是不確定的,取決與  和 寫(xiě) 的操作是以怎樣的順序進(jìn)行操作的。為了避免這個(gè)問(wèn)題,“小說(shuō)”服務(wù)使用了線程安全的 ConcurrentMap。這個(gè)映射的關(guān)鍵是生成了一個(gè)線程安全的 AtomicInteger。下面是相關(guān)的代碼片段:

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

默認(rèn)情況下,對(duì)客戶端請(qǐng)求的響應(yīng)被編碼為 XML。為了簡(jiǎn)單,“小說(shuō)”程序使用了以前的 XMLEncoder 類(lèi);另一個(gè)包含更豐富功能的方式是使用 JAX-B 庫(kù)。代碼很簡(jiǎn)單:

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 參數(shù)要么是一個(gè)有序的“小說(shuō)” ArraList(用以響應(yīng)“ 獲得所有(get all)”請(qǐng)求),要么是一個(gè) Novel 實(shí)例(用以響應(yīng)“ 獲得一個(gè)(get one)”請(qǐng)求),又或者是一個(gè) String(確認(rèn)消息)。

如果 HTTP 請(qǐng)求報(bào)頭指定 JSON 作為所需要的類(lèi)型,那么 XML 就被轉(zhuǎn)化成 JSON。下面是 NovelsServlet 中的 doGet 方法中的檢查:

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

Novels類(lèi)中包含了 toJson 方法,可以將 XML 轉(zhuǎn)換成 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會(huì)對(duì)各種類(lèi)型進(jìn)行錯(cuò)誤檢查。比如,POST 請(qǐng)求應(yīng)該包含新“小說(shuō)”的作者和標(biāo)題。如果有一個(gè)丟了,doPost 方法會(huì)拋出一個(gè)異常:

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

SC_BAD_REQUEST 中的 SC 代表的是 狀態(tài)碼(status code),BAD_REQUEST 的標(biāo)準(zhǔn) HTTP 數(shù)值是 400。如果請(qǐng)求中的 HTTP 動(dòng)詞是 TRACE,會(huì)返回一個(gè)不同的狀態(tài)碼:

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

測(cè)試“小說(shuō)”服務(wù)

用瀏覽器測(cè)試 web 服務(wù)會(huì)很不順手。在 CRUD 動(dòng)詞中,現(xiàn)代瀏覽器只能生成 POST(創(chuàng)建)和 GET(讀?。┱?qǐng)求。甚至從瀏覽器發(fā)送一個(gè) POST 請(qǐng)求都有點(diǎn)不好辦,因?yàn)閳?bào)文需要包含鍵-值對(duì);這樣的測(cè)試通常通過(guò) HTML 表單完成。命令行工具,比如說(shuō) curl ,是一個(gè)更好的選擇,這個(gè)部分展示的一些 curl 命令,已經(jīng)包含在我網(wǎng)站的 ZIP 文件中了。

下面是一些測(cè)試樣例,沒(méi)有展示相應(yīng)的輸出結(jié)果:

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

第一條命令請(qǐng)求所有“小說(shuō)”,默認(rèn)是 XML 編碼。第二條命令請(qǐng)求 ID 為 1 的“小說(shuō)”,XML 編碼。最后一條命令通過(guò) application/json 添加了 Accept 報(bào)頭元素,作為所需要的 MIME 類(lèi)型。“ 獲得一個(gè)(get one)”命令也可以用這個(gè)報(bào)頭。這些請(qǐng)求用了 JSON 而不是 XML 編碼作為響應(yīng)。

下面兩條命令在集合中創(chuàng)建了一個(gè)新“小說(shuō)”,并且確認(rèn)添加了進(jìn)去:

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

curl 中的 PUT 命令與 POST 命令相似,不同的地方是 PUT 的報(bào)文沒(méi)有使用標(biāo)準(zhǔn)的語(yǔ)法。在 NovelsServlet 中關(guān)于 doPut 方法的文檔中有詳細(xì)的介紹,但是簡(jiǎn)單來(lái)說(shuō),Tomcat 不會(huì)對(duì) PUT 請(qǐng)求生成合適的映射。下面是一個(gè) PUT 命令和確認(rèn)命令的的例子:

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

第二個(gè)命令確認(rèn)了集合已經(jīng)更新。

最后,DELETE 命令會(huì)正常運(yùn)行:

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

這個(gè)請(qǐng)求是刪除 ID 為 2 的“小說(shuō)”。第二個(gè)命令會(huì)顯示剩余的“小說(shuō)”。

web.xml 配置文件

盡管官方規(guī)定它是可選的,web.xml 配置文件是一個(gè)生產(chǎn)級(jí)別網(wǎng)站或服務(wù)的重要組成部分。這個(gè)配置文件可以配置獨(dú)立于代碼的路由、安全性,或者網(wǎng)站或服務(wù)的其他功能。“小說(shuō)”服務(wù)的配置通過(guò)為該服務(wù)的請(qǐng)求分配一個(gè) URL 模式來(lái)配置路由:

<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)提供了一個(gè)縮寫(xiě)(novels),然后這個(gè)名字在下面的 servlet-mapping 元素中使用。

回想一下,一個(gè)已部署服務(wù)的 URL 會(huì)在端口號(hào)后面有 WAR 文件的文件名:

http://localhost:8080/novels/

端口號(hào)后斜杠后的 URI,是所請(qǐng)求資源的“路徑”,在這個(gè)例子中,就是“小說(shuō)”服務(wù)。因此,novels 出現(xiàn)在了第一個(gè)單斜杠后。

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

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

web.xml 配置也會(huì)指定這個(gè)請(qǐng)求被分配到“小說(shuō)” servlet 中,因?yàn)?/* 模式也包含 /foobar。因此,這個(gè)不存在的 URL 也會(huì)得到像上面合法路徑的相同結(jié)果。

生產(chǎn)級(jí)別的配置文件可能會(huì)包含安全相關(guān)的信息,包括 連接級(jí)別(wire-level)和 用戶角色(users-roles)。即使在這種情況下,配置文件的大小也只會(huì)是這個(gè)例子中的兩到三倍大。

總結(jié)

HttpServlet 是 Java web 技術(shù)的核心。像“小說(shuō)”這樣的網(wǎng)站或 web 服務(wù)繼承了這個(gè)類(lèi),并且根據(jù)需求重載了相應(yīng)的 do 動(dòng)詞方法。像 Jersay(JAX-RS)或 Restlet 這樣的 Restful 框架通過(guò)提供定制的 servlet 完成了基本相同的功能,針對(duì)框架中的 web 應(yīng)用程序的請(qǐng)求,這個(gè) servlet 扮演著 HTTP(S) 終端(endpoint)的角色。

當(dāng)然,基于 servlet 的應(yīng)用程序可以訪問(wèn) web 應(yīng)用程序中所需要的任何 Java 庫(kù)。如果應(yīng)用程序遵循 關(guān)注點(diǎn)分離(separation-of-concerns)原則,那么 servlet 代碼仍然相當(dāng)簡(jiǎn)單:代碼會(huì)檢查請(qǐng)求,如果存在缺陷,就會(huì)發(fā)出適當(dāng)?shù)腻e(cuò)誤;否則,代碼會(huì)調(diào)用所需要的功能(比如,查詢數(shù)據(jù)庫(kù),以特定格式為響應(yīng)編碼),然后向請(qǐng)求這發(fā)送響應(yīng)。HttpServletRequest 和 HttpServletReponse 類(lèi)型使得讀取請(qǐng)求和編寫(xiě)響應(yīng)變得簡(jiǎn)單。

Java 的 API 可以從非常簡(jiǎn)單變得相當(dāng)復(fù)雜。如果你需要用 Java 交付一些 Restful 服務(wù)的話,我的建議是在做其他事情之前先嘗試一下簡(jiǎn)單的 HttpServlet。


via: opensource.com

作者: Marty Kalin 選題: lujun9972 譯者: Yufei-Yan 校對(duì): wxy

本文由 LCTT 原創(chuàng)編譯, Linux中國(guó) 榮譽(yù)推出

分享到:
標(biāo)簽:服務(wù) Web
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定