网络服务以各种形式存在了二十多年。 例如,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 启动后,在浏览器中输入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
。
某些请求(特别是 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,则该请求将被解释为“获取指定的一个”
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。 以下是 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=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,该 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 代码仍然非常简单:代码检查请求,并在存在缺陷时发出相应的错误; 否则,代码会调用可能需要的任何功能(例如,查询数据库,以指定格式编码响应),然后将响应发送给请求者。 HttpServletRequest
和 HttpServletResponse
类型使执行读取请求和编写响应的 servlet 特定工作变得容易。
Java 具有从非常简单到非常复杂的 API。 如果您需要使用 Java 交付一些 Restful 服务,我的建议是在尝试其他任何操作之前,先尝试一下低调的 HttpServlet
。
评论已关闭。