Java 中非常轻量级的 RESTful Web 服务示例

通过一个完整的代码示例来探索 Java 中轻量级的 RESTful 服务,以管理图书收藏。
125 位读者喜欢这篇文章。
Coding on a computer

网络服务以各种形式存在了二十多年。 例如,XML-RPC 服务 出现在 1990 年代后期,紧随其后的是用 SOAP 分支编写的服务。REST 架构风格 的服务也大约在二十年前出现,紧随 XML-RPC 和 SOAP 的先驱之后。REST 风格(以下简称 Restful)服务现在在 eBay、Facebook 和 Twitter 等热门网站中占据主导地位。 尽管分布式计算的网络服务有其他替代方案(例如,网络套接字、微服务和用于远程过程调用的新框架),但 Restful 网络服务仍然因以下几个原因而具有吸引力

  • Restful 服务建立在现有的基础设施和协议之上,特别是 Web 服务器和 HTTP/HTTPS 协议。 拥有基于 HTML 的网站的组织可以轻松地为对数据和底层功能比 HTML 呈现更感兴趣的客户端添加 Web 服务。 例如,亚马逊率先通过网站和 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,这是一个坚固而灵活的软件,自 1990 年代后期的早期企业 Java 以来就已存在。 servlet 充当客户端 CRUD 请求的 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 原样。 可以通过删除 WAR 文件来删除网站或服务,并通过使用新版本覆盖 WAR 文件来更新网站或服务。 顺便说一句,调试网站或服务的第一步是检查 Tomcat 是否已解压缩 WAR 文件; 如果没有,则说明由于代码或配置中的致命错误而未发布该站点或服务。

  • 由于 Tomcat 默认在端口 8080 上侦听 HTTP 请求,因此本地计算机上 Tomcat 的请求 URL 以

    http://localhost:8080/

    通过添加 WAR 文件的名称(但不带 .war 扩展名)来访问程序员部署的 WAR 文件

    http://locahost:8080/novels/

    如果服务部署在 TOMCAT_HOME 的子目录(例如 myapps)中,则这将反映在 URL 中

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

    我将在本文结尾附近的测试部分提供有关此内容的更多详细信息。

如前所述,我的主页上的 ZIP 文件包含一个 Ant 脚本,该脚本编译和部署网站或服务。 (ZIP 文件中还包含 novels.war 的副本。) 对于小说示例,示例命令(以 % 作为命令行提示符)是

% ant -Dwar.name=novels deploy

此命令编译 Java 源文件,然后构建一个名为 novels.war 的可部署文件,将此文件保留在当前目录中,并将其复制到 TOMCAT_HOME/webapps。 如果一切顺利,GET 请求(使用浏览器或命令行实用程序(例如 curl))将作为首次测试

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

Tomcat 默认配置为热部署: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 请求动词(官方名称为方法)命名的空方法

  • doPost (Post = 创建)
  • doGet (Get = 读取)
  • doPut (Put = 更新)
  • doDelete (Delete = 删除)

还涵盖了一些其他 HTTP 动词。 HttpServlet 的扩展(例如 NovelsServlet)会覆盖任何感兴趣的 do 方法,而将其余方法保留为无操作。 NovelsServlet 覆盖了七个 do 方法。

每个 HttpServlet CRUD 方法都采用相同的两个参数。 以下是 doPost 的示例

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 参数是 HTTP 请求信息的映射,response 提供返回给请求者的输出流。 诸如 doPost 之类的方法的结构如下

  • 读取 request 信息,采取任何适当的操作以生成响应。 如果信息丢失或存在其他缺陷,则生成错误。
  • 使用提取的请求信息来执行适当的 CRUD 操作(在本例中为创建 Novel),然后使用 response 输出流将适当的响应编码给请求者。 对于 doPost,响应是确认已创建新小说并将其添加到集合中。 发送响应后,输出流将关闭,这也将关闭连接。

有关 do 方法覆盖的更多信息

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)和 URI(统一资源标识符)开头,URI 是命名目标资源的名词(在本例中为 novels)。 标头由键值对组成,冒号将左侧的键与右侧的值分隔开。 带有键 Host(不区分大小写)的标头是必需的; 主机名 localhost 是本地计算机上本地计算机的符号地址,端口号 8080 是等待 HTTP 请求的 Tomcat Web 服务器的默认端口号。 (默认情况下,Tomcat 在端口 8443 上侦听 HTTPS 请求。) 标头元素可以以任意顺序出现。 在此示例中,Accept-type 标头的值是 MIME 类型 text/plain

某些请求(特别是 POSTPUT)具有正文,而另一些请求(特别是 GETDELETE)没有正文。 如果存在正文(可能为空),则两个换行符将标头与正文分开; HTTP 正文由键值对组成。 对于无正文请求,可以使用标头元素(例如查询字符串)来发送信息。 以下是使用 ID 2 GET /novels 资源的请求

GET /novels?id=2

查询字符串以问号开头,通常由键值对组成,尽管键可以没有值。

HttpServlet 具有 getParametergetParameterMap 等方法,可以很好地隐藏有正文和无正文的 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

一些棘手的细节

小说服务设计反映了基于 Java 的 Web 服务器(例如 Tomcat)的工作方式。 在启动时,Tomcat 构建一个线程池,从中提取请求处理程序,这种方法称为每个请求一个线程模型。 现代版本的 Tomcat 还使用非阻塞 I/O 来提高性能。

小说服务作为 NovelsServlet 类的单个实例执行,而 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 参数可以是小说的排序 ArrayList(响应“获取全部”请求); 或者单个 Novel 实例(响应获取一个请求); 或 String(确认消息)。

如果 HTTP 请求标头将 JSON 称为所需的类型,则 XML 将转换为 JSON。 以下是 NovelsServletdoGet 方法中的检查

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 中的 SC_BAD_REQUEST 代表状态代码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 编码。 最后一个命令添加一个 Accept 标头元素,其中 application/json 作为所需的 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 正文不使用标准语法。 NovelsServletdoPut 方法的文档对此进行了详细介绍,但简而言之,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,该 URI 称为请求资源的路径,在本例中为小说服务; 因此,术语 novels 出现在第一个单斜杠之后。

web.xml 文件中,url-pattern 指定为 /*,这意味着任何以 /novels 开头的路径。 假设 Tomcat 遇到一个人为设计的请求 URL,例如

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

web.xml 配置指定此请求也应调度到小说 servlet,因为 /* 模式涵盖 /foobar。 因此,人为设计的 URL 的结果与上面显示的合法 URL 的结果相同。

生产级配置文件可能包含有关安全性的信息,包括线路级安全性和用户角色安全性。 即使在这种情况下,配置文件的大小也仅是示例文件大小的两到三倍。

总结

HttpServlet 是 Java Web 技术的核心。 网站或 Web 服务(例如小说服务)扩展此类,覆盖感兴趣的 do 动词。 Restful 框架(例如 Jersey (JAX-RS) 或 Restlet)通过提供自定义 servlet 来实现基本相同的目的,该 servlet 随后充当针对在框架中编写的 Web 应用程序的 HTTP(S) 端点。

基于 servlet 的应用程序当然可以访问 Web 应用程序中所需的任何 Java 库。 如果应用程序遵循关注点分离原则,则 servlet 代码仍然非常简单:代码检查请求,并在存在缺陷时发出相应的错误; 否则,代码会调用可能需要的任何功能(例如,查询数据库,以指定格式编码响应),然后将响应发送给请求者。 HttpServletRequestHttpServletResponse 类型使执行读取请求和编写响应的 servlet 特定工作变得容易。

Java 具有从非常简单到非常复杂的 API。 如果您需要使用 Java 交付一些 Restful 服务,我的建议是在尝试其他任何操作之前,先尝试一下低调的 HttpServlet

下一步阅读
标签
User profile image.
我是计算机科学领域的学者(德保罗大学计算与数字媒体学院),在软件开发方面拥有广泛的经验,主要是在生产计划和调度(钢铁行业)以及产品配置(卡车和公共汽车制造)方面。 有关书籍和其他出版物的详细信息,请访问

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.