怎么理解用Java实现的超轻量级RESTful Web服务

发布时间:2021-10-28 16:53:46 作者:iii
来源:亿速云 阅读:96

本篇内容主要讲解“怎么理解用Java实现的超轻量级RESTful Web服务”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么理解用Java实现的超轻量级RESTful Web服务”吧!

Web 服务,以这样或那样的形式,已经存在了近二十年。比如,XML-RPC 服务出现在 90 年代后期,紧接着是用 SOAP 分支编写的服务。在 XML-RPC 和 SOAP 这两个开拓者之后出现后不久,REST 架构风格的服务在大约 20 年前也出现了。REST 风格(以下简称  Restful)服务现在主导了流行的网站,比如 eBay、Facebook 和 Twitter。尽管分布式计算的 Web 服务有很多替代品(如  Web 套接字、微服务和远程过程调用的新框架),但基于 Restful 的 Web 服务依然具有吸引力,原因如下:

基于 Restful 的“小说” Web 服务

基于 Restful 的“小说” web 服务包含三个程序员定义的类:

一些 Java 框架,比如 Jersey(JAX-RS)和 Restlet,就是为 Restful 服务设计的。尽管如此,HttpServlet 本身为完成这些服务提供了轻量、灵活、强大且充分测试过的 API。我会通过下面的“小说”例子来说明。

部署“小说” Web 服务

当然,部署“小说” Web 服务需要一个 Web 服务器。我的选择是 Tomcat,但是如果该服务托管在 Jetty 或者甚至是 Java 应用服务器上,那么这个服务应该至少可以工作(著名的最后一句话!)。在我的网站上有总结了如何安装 Tomcat 的 README 文件和代码。还有一个附带文档的 Apache Ant 脚本,可以用来构建“小说”服务(或者任何其他服务或网站),并且将它部署在 Tomcat 或相同的服务。

Tomcat 可以从它的官网上下载。当你在本地安装后,将 TOMCAT_HOME 设置为安装目录。有两个子目录值得关注:

通过添加不带 .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)命名的空方法:

其他一些 HTTP 动词也会涉及到。HttpServlet 的子类,比如 NovelsServlet,会重载相关的 do 方法,并且保留其他方法为no-opsNovelsServlet 重载了七个 do 方法。

每个 HttpServlet 的 CRUD 方法都有两个相同的参数。下面以 doPost 为例:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 参数是一个 HTTP 请求信息的映射,而 response 提供了一个返回给请求者的输出流。像 doPost 的方法,结构如下:

关于方法重载的更多内容

HTTP 请求的格式相对比较简单。下面是一个非常熟悉的 HTTP 1.1 的格式,注释由双井号分隔:

GET /novels              ## start lineHost: localhost:8080     ## header elementAccept-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

一些请求(特别是 POST 和 PUT)会有报文,而其他请求(特别是 GET 和 DELETE)没有。如果有报文(可能为空),以两个换行符将报头和报文分隔开;HTTP 报文包含一系列键-值对。对于无报文的请求,比如说查询字符串,报头元素就可以用来发送信息。下面是一个用 ID 2 对 /novels 资源的 GET 请求:

GET /novels?id=2

通常来说,查询字符串以问号开始,并且包含一个键-值对,尽管这个键-值可能值为空。

带有 getParameter 和 getParameterMap 等方法的 HttpServlet 很好地回避了有报文和没有报文的 HTTP 请求之前的差异。在“小说”例子中,getParameter 方法用来从 GETPOST 和 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

一些值得注意的细节

“小说”服务的设计反映了像 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 codeBAD_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&amp;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。即使在这种情况下,配置文件的大小也只会是这个例子中的两到三倍大。

到此,相信大家对“怎么理解用Java实现的超轻量级RESTful Web服务”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

推荐阅读:
  1. RestFul架构介绍
  2. Python实现Restful API的例子

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java

上一篇:Python编程语言的特点有哪些

下一篇:Mysql数据分组排名实现的示例分析

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》