网络服务以各种形式存在了二十多年。 例如,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.sh和shutdown.sh)和 Windows(startup.bat和shutdown.bat)的启动和停止脚本。 Tomcat 作为 Java 应用程序运行。 Web 服务器的 servlet 容器名为 Catalina。 (在 Jetty 中,Web 服务器和容器具有相同的名称。) Tomcat 启动后,在浏览器中输入https://: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 以
https://: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 https://: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。
某些请求(特别是 POST 和 PUT)具有正文,而另一些请求(特别是 GET 和 DELETE)没有正文。 如果存在正文(可能为空),则两个换行符将标头与正文分开; HTTP 正文由键值对组成。 对于无正文请求,可以使用标头元素(例如查询字符串)来发送信息。 以下是使用 ID 2 GET /novels 资源的请求
GET /novels?id=2查询字符串以问号开头,通常由键值对组成,尽管键可以没有值。
HttpServlet 具有 getParameter 和 getParameterMap 等方法,可以很好地隐藏有正文和无正文的 HTTP 请求之间的区别。 在小说示例中,getParameter 方法用于从 GET、POST 和 DELETE 请求中提取所需的信息。 (处理 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,则该请求将被解释为“获取指定的一个”
https://:8080/novels?id=2 ## GET specified如果没有查询字符串,则 GET 请求将被解释为“获取全部”
https://: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。 以下是 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 中的 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=4curl 中的 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 文件名
https://:8080/novels/端口号后面的斜杠立即开始 URI,该 URI 称为请求资源的路径,在本例中为小说服务; 因此,术语 novels 出现在第一个单斜杠之后。
在 web.xml 文件中,url-pattern 指定为 /*,这意味着任何以 /novels 开头的路径。 假设 Tomcat 遇到一个人为设计的请求 URL,例如
https://: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 代码仍然非常简单:代码检查请求,并在存在缺陷时发出相应的错误; 否则,代码会调用可能需要的任何功能(例如,查询数据库,以指定格式编码响应),然后将响应发送给请求者。 HttpServletRequest 和 HttpServletResponse 类型使执行读取请求和编写响应的 servlet 特定工作变得容易。
Java 具有从非常简单到非常复杂的 API。 如果您需要使用 Java 交付一些 Restful 服务,我的建议是在尝试其他任何操作之前,先尝试一下低调的 HttpServlet。

评论已关闭。