第六章 RESTFUL体系结构风格

在本章中,我们将研究RESTful体系结构风格的含义,然后我们将在这个上下文中研究RESTful服务。

6.1 RESTFUL架构风格

根据Fielding(2000)的研究,RESTful架构风格的关注点在于:

组件的角色,它们与其他组件交互的约束,以及它们对重要数据元素的解释……

RESTful架构风格包括对数据的约束、对数据解释的约束、对组件的约束以及组件之间连接器的约束。

让我们首先看看在RESTful架构风格的上下文中对信息和数据的解释,这个视图由资源和表示组成(resources and representations)

使用RESTful架构风格时,信息被描述为资源,即定义为超文本引用的目标。资源可以是空的,也可以是实体的集合,资源可以是静态的, 也可以随时间变化。一个资源通过其映射(即其超文本引用)与另一个资源区分开来。Fielding给出了一个示例,即源代码的某个版本可以有一个超文本引用,表示它是最新版本,还可以有一个单独的超文本引用,引用版本号1.2.7”。请注意,当修订版1.2.7首先发布,超文本最新修订版可以指代版本号1.2.7”所指的相同代码。但是,这是一个临时条件,因为在发布1.2.8修订版时,超文本最新版本将不再引用版本号1.2.7”

用于指定资源目标的超文本引用采用URLURN格式。

RESTful架构风格中的表示由用于描述数据的数据和元数据组成(Fielding还提到有时使用附加元数据来描述元数据 以确保消息完整性)。通常,表示将指代HTTP消息及其内容,或者替代地是文档或文件(HTTP消息当然可以包括文档或文件)。表示中可以包含控制数据(在这种情况下是HTTPmessage)指定消息的用途。表示中的数据格式由Internet媒体类型(MIME类型)定义。表示的示例是JPEG图像或HTML文档。

RESTful体系结构风格中的“表示”由用于描述数据的数据和元数据组成(Fielding还提到,有时还使用额外的元数据来描述元数据,以确保消息的完整性)。通常,“表示”指的是HTTP消息及其内容,或者文档或文件(HTTP消息当然可以包含文档或文件)。可以在指定消息用途的表示(在本例中是HTTP消息)中包含控制数据。表示形式中的数据格式由Internet媒体类型(MIME类型)定义。“表示”的一个例子是JPEG图像或HTML文档。

最简单的REST组件是用户代理”——例如,使用客户机连接器访问某种服务器的web浏览器。被访问的服务器将使用服务器连接器进行回复。(接下来我们将讨论连接器。)另一种组件是代理组件,它在客户机和服务器组件之间转发请求和响应。

RESTful体系结构风格包括“连接器”,这些连接器定义了如何访问资源以及如何传输资源的表示。连接器的概念用于隐藏底层网络问题,因此连接器是通信抽象。RESTful连接器的示例包括:“客户机”、“服务器”、“缓存”和“解析器”。“客户机”发出请求,而“服务器”侦听连接并返回响应。客户端或服务器都可以使用“缓存”来保存信息,这样就不必重新生成信息,或者在信息自上一代或传输以来没有发生变化的情况下,也不必再次通过网络发送信息。“解析器”是将名称转换为完整地址的地址转换器。

因此,当考虑HTML web页面时,这是一个资源(因为它位于URL)并包含一个表示(HTML本身)。在HTTP响应消息中传输的internet媒体类型以及此HTML数据将是“text/ HTML”,这是表示元数据的一个示例。RESTful架构风格具有以下约束:

•客户机/服务器:关注点分离,以客户机-服务器体系结构为例。其思想是不同的组件可以独立地发展—客户机中的用户界面可以独立于服务器发展,服务器也更简单。

•无状态:客户机-服务器交互是无状态的。服务器上没有存储上下文。任何会话信息都必须由客户机保存。

Cacheable:响应(对之前请求的响应)中的数据被标记为cache-ablenon-cache-able。如果它是可缓存的,客户端(或中介)将来可能会重用它来处理相同类型的请求。

•统一接口:组件之间有统一的接口。在实践中,有四个接口约束:

资源标识请求标识它们正在操作的资源(例如,通过URI)

通过资源表示进行资源操作当客户端或服务器访问资源时,基于对资源表示的理解,它有足够的信息来修改该资源

消息是自描述的消息包含足够的信息,允许客户端或服务器处理消息,这通常通过使用Internet媒体类型(MIME类型)来完成

使用超媒体改变应用程序的状态例如,服务器提供超链接,客户端使用超链接进行状态转换

分层系统:组件被组织在分层的层中,组件只知道交互发生的层。因此,连接到服务器的客户机不知道任何中间连接。

中间过滤器组件可以在消息传输过程中更改消息:因为消息是自描述的,而且语义是可见的,所以过滤器组件对消息有足够的了解,可以对其进行修改

代码随需应变:可选地支持代码随需应变,即客户端可以下载扩展其功能的脚本。

因此,RESTful web服务是基于客户机/服务器的,不存储状态。从客户机到服务器的请求的结果可以缓存在客户机中。它具有基于超媒体的统一接口,具有自描述消息。而且客户机和服务器并不知道它们之间的中间连接。

注意,这些是万维网的特征它基于客户机/服务器,主要由浏览器和web服务器组成。每个对web服务器的调用都被认为独立于另一个这样的调用。它的统一接口由HTTP协议提供。数据格式由包含在消息中的Internet媒体类型指定,因此消息是自描述的。等等。

Fielding等人(1999)根据RESTful架构风格的原则设计了HTTP 1.1协议。

现在,让我们根据上面讨论的约束来考虑RESTful web服务。

如果您看一下我们将在后面面讨论的一些RESTful技术:AJAX (XMLHttpRequest)ServletJAX-RS,这些技术都是通过寻址url来工作的。它们都使用HTTP消息(getpost)发送消息,消息中的数据使用MIME类型进行描述。因此,它们服从统一的接口约束。它们充当客户机和服务器。它们可以以无状态的方式进行操作(请注意,它们也可以以有状态的方式进行操作),因此,至少当它们选择无状态操作时,它们满足无状态约束。

他们了解HTTP层,这是实现他们的统一接口的地方,但是他们透明地对待较低的internet层,所以他们不知道任何中间较低级别的节点。如果需要,客户端可以缓存数据。它支持按需编码,例如,html web页面中的<script>标记中的JavaScript可以访问RESTful接口(使用XMLHttpRequestAJAX就是一个很好的例子)

因此,RESTful web服务显然满足RESTful体系结构的约束。

总之,RESTful web服务是基于客户机/服务器的,不存储状态。它访问位于URL的资源(web页面或数据)。从客户机到服务器的请求的结果可以缓存在客户机中。它具有基于超媒体的统一接口,具有自描述消息。而且客户机和服务器并不知道它们之间的中间连接。这些是万维网的特性。它基于客户机/服务器,主要由浏览器和web服务器组成。每个对web服务器的调用都被认为独立于另一个这样的调用(除非您正在使用cookieURL重定向之类的东西,否则我们将在本章后面讨论这些)。它的统一接口由HTTP协议提供。数据格式由包含在消息中的Internet媒体类型指定,因此消息是自描述的。等等。

简而言之,RESTful web服务利用了万维网的基本功能。

然而,一般来说,谈到RESTful web服务,就行业实践中的一般含义而言在行业中,RESTful web服务是使用HTTP消息(getpost)定义的任何东西,通常情况下,开发人员称为RESTful接口的接口并不总是遵循“无状态”约束,例如,他们可以使用本质上是有状态概念的cookie。此外,虽然通常使用Internet媒体类型,但是有些RESTful接口可能不需要发送这种元数据信息。

6.2 RESTful Web服务

现在让我们简要地重新审视"RESTful"的含义。

回想一下6.1RESTful架构风格背后的主要思想是

 客户端/服务器:关注点分离,例如客户端/服务器架构。我们的想法是,不同的组件可以独立演化—客户端中的用户界面可以与服务器分开演化,服务器也更简单。

 无状态:客户端服务器交互是无状态的服务器上存储会话的上下文客户必须自己维护会话信息。

 可缓存:响应中的数据(对先前请求的响应)被标记为可缓存或不可缓存。如果它是可缓存的,则客户端(或中间节点)可以在将来重用该类型的请求。

 统一接口:组件之间有统一的接口。在实践中,有四个接口约束:

 资源标识请求(Requests)标识它们正在操作的资源(例如,通过URI)

 通过资源的表示对资源进行操纵—当客户机或服务器访问资源时,基于对资源表示形式的理解,它拥有足够的信息来修改该资源

 消息是自描述的消息包含足够的信息,允许客户机或服务器处理消息,这通常通过使用Internet媒体类型(MIME类型)来完成

 使用超媒体来更改应用程序的状态例如,服务器提供客户端用于进行状态转换的超链接

 分层系统:组件按层次结构组织,组件仅知道正在进行交互的层。因此,连接到服务器的客户端不知道任何中间连接。

 中间过滤器组件可以在传输过程中更改消息:因为message是自描述的,并且语义是可见的,过滤器组件对消息的理解足以对其进行修改

 按需代码:缺省支持,即客户端可以下载脚本扩展他们的功能。

因此,RESTful Web服务是基于客户端/服务器的,并且不存储状态。客户端到服务器的请求结果可以缓存在客户端中。它具有基于超媒体的自描述消息的统一接口。哦,客户端和服务器不知道他们两个之间的中间连接。

正如我们之前在6.1节中讨论的那样,这些是万维网的特征它基于客户端/服务器,主要由浏览器和Web服务器组成Web服务器的每次调用都被认为独立于另一个此类调用(除非您使用的是CookieURL重定向之类的内容)。它的统一接口由HTTP协议提供数据格式由Internet媒体类型指定,它们包含在消息中,因此消息是自描述的。等等。

简而言之,RESTful Web服务利用了万维网的基本功能:它们使用HTTP协议,并且它们使用Internet媒体类型。一般来说,他们是无状态的。好的,现在我们从总体上都知道我们在谈论什么。在下面的部分中,我们将讨论与RESTful Web服务相关的多种技术。我们将从异步JavaScriptXML(AJAX)开始,这很容易理解,是一个很好的入门起点,然后我们接着讨论Java Servlet,它可以用作REST应用Web服务JAX-RSJava应用程序编程接口(API)的支持技术(JAX-RS可以部署在Servlet)(Java Servlets本身也可以以RESTful方式使用)Java Servlets可以追溯到20世纪90年代末的Sun Microsystems。然后我们将看看JAX-RS

6.3 技术回顾:异步JAVASCRIPTXML(AJAX)

异步JavaScriptXML (AJAX)是在客户机上一起使用的技术集合的名称,在这种技术框架下,用户的web页面可以在后台一次更新页面需要更新的部分(整个web页面不会更改)在更新的同时用户可以在页面上继续进行其他工作。

当在浏览器和web服务器之间使用"标准的"HTTP连接时,连接是同步的,浏览器web服务器提供其新的信息之前是处于阻塞状态的,并且一次更新整个web页面。

James Garrett最初使用术语"AJAX"来指代一系列技术他认为属于AJAX的各种技术包括

 Web页面表示技术,如XHMTLCSS

 使用文档对象模型进行动态显示

 使用XML进行数据交换(他包括XSLT,用于将一个XML文档转换成另一个XML文档,或转换成其他格式,如文本)

 web客户机上运行JavaScript来处理后台处理和所有数据传输(Garrett的想法中,JavaScript"将所有东西绑定在一起",即所有其他技术)

 使用XMLHttpRequest进行异步数据检索(更新web页面的一部分而不是整个web页面)

AJAX背后的想法是,在加载初始网页后,客户端上运行的JavaScript代码尽可能地处理用户输入和网页更新(例如,简单的数据验证或简单的数据编辑可以在本地客户端处理)。当需要来自服务器的数据时,仅从服务器检索该特定数据,并且仅将其写入网页的一部分。整个网页保持不变。

Web服务通信方面,当必须从服务器检索数据时,AJAX中使用的主要技术是XMLHttpRequest。在使用XMLHttpRequest从服务器检索数据之后,Web客户端上的JavaScript使用文档对象模型(DOM)来更新网页的相应部分的内容,最初Web服务器Web客户端交互数据使用XML格式,但今天AJAX更常用的是JSON数据。

XMLHttpRequest是一个API(从在Web客户端上运行的JavaScript访问),用于构建要发送到Web服务器的HTTPHTTPS请求,并将Web服务器返回的数据(来自关联的HTTP响应)传递回客户端上的JavaScript代码。

使用XMLHttpRequestAJAX被认为是RESTful,因为客户端和服务器之间的所有通信都是通过HTTP(HTTPS)请求/响应来完成的

XMLHttpRequest的前身是由Microsoft2000年实现的,并且在其他浏览器中成为了事实上的标准。自2006年以来,万维网联盟(W3C)提供了各种草案版本。

6.3.1 简单的XMLHTTPREQUEST示例

在清单6.1中,我们看到了一个使用XMLHttpRequest的简单示例PHP编写的一个非常简单的处理这个响应的服务器代码如清单6.2所示:它将返回包含以下内容的XML数据:

"Hi, (name you typed in)! This is new important data for your web page"

LISTING 6.1 Extremely Simple XMLHttpRequest Client

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 id="myHeader"> This is an AJAX example</h1>
<p> This code describes how to do an AJAX example </p>
<p id="thispart"> This is the part that is updated </p>
<p> It cleverly updates only part of a web page </p>
<form action="simple_server.php">
    <p> Enter your name: <input type="text" name="myname"/>
        <button type="button" onclick="doAJAXstuff(this.form)">
            Paint part of the web page
        </button>
    </p>
</form>
<script type="text/javascript">
    function doAJAXstuff(theform) {
        request = new XMLHttpRequest();
        request.open("GET", "simple_server.php?mydata=" + theform.myname.value);
        request.onreadystatechange = function () {
            if (request.readyState == 4)
                my_callback();
        }
        request.send();
    }

    function my_callback() {
        if (request.status == 200) {
            document.getElementById("thispart").innerHTML = request.responseText;
            document.getElementById("thispart").style.font = "italic bold 20px arial,serif";
            alert("There wasn't an error, just wanted to let you see how an alert works." +
                "Received from the server:\n"
                + request.responseText);
        }
        else if (request.status == 404) {
            alert("Server was not found: data returned is :\n" + request.responseText);
        }
    }
</script>
</body>
</html>

XMLHttpRequest用于构建HTTP(HTTPS)请求。首先,构建一个JavaScript对象来保存XMLHttpRequest

request = new XMLHttpRequest();

接下来,我们在XMLHttpRequest对象上调用"open"Open具有以下格式

request.open(HTTP method type, url being called, asynchronous or not)

通常,使用的HTTP方法类型是"GET""POST""PUT",但您也可以使用"HEAD""PUT""DELETE"

还可能有其他用户名和密码参数。

当你将asynchronous指定为TRUE(这应该是默认的,虽然我在浏览器中发现了一些情况似乎并非如此),但请注意清单6.1中使用了默认值,因为该参数尚未指定),然后JavaScript代码将不会阻塞和等待来自服务器的响应相反,它继续执行。稍后我们将看到另一个示例,它清楚地说明了一个异步示例。

request.send()向服务器发送HTTP请求这里没有其他数据要发送。您之前在HTML表单中输入的名称将作为GET查询字符串发送。如果有任何附加数据,它将作为参数包含在send()函数中。

XMLHttpRequest还有另一种方法,叫做setRequestHeader。这个示例中没有显示,但可以用于在HTTP请求中添加其他标头。

发送后,本地网页(包括JavaScript)可以继续执行,直到从服务器返回回复。与此同时,onreadystatechange事件侦听器正在等待来自服务器的响应。

LISTING 6.2 Extremely Simple PHP Server

<?php

$mydata = $_REQUEST["mydata"];

print "<p>Hi, $mydata! This is new important data for your web page.</p>";

?>

当响应(最终)从服务器到达时,调用与onreadystatechange关联的函数:

request.onreadystatechange = function ()

{

if (request.readyState == 4)

my_callback();

}

这将检查请求是否已完成(request.readyState等于4)。如果是,则调用my_callback函数来处理请求。

my_callback函数检查HTTP响应代码。如果这等于200,则请求成功,从服务器接收的数据是良好的数据,并且使用DOM将其写入整个网页中间的位置,而不必重写整个页面。

在这里它也会弹出一个"提示窗口",只是为了告诉你如何

如果由于某种原因网页不可用,my_callback函数将收到404响应代码,在这种情况下将生成错误警报。(注意:你可以通过重命名我给你的.php文件强制这种情况发生)

此示例显示仅更新网页的一部分的网页。图6.1显示了在按下按钮之前的网页。

6.2显示了按下按钮后的网页请注意,更新了一行,但网页的其余部分与图6.1中的相同。

6.1 在运行一个非常简单的XMLHttpRequest示例之前。

6.2 在运行非常简单的XMLHttpRequest示例之后。

6.3.2 响应代码示例

XMLHttpRequest提供五种不同的readyState响应代码:

0:请求未初始化

1:已建立服务器连接

2:服务器收到请求

3:服务器正在处理请求

4:服务器已完成请求,现在响应已准备就绪

我们在清单6.1中看到了成功的响应代码处理(request.readyState == 4)

其他响应代码显示的是响应的中间状态。清单6.3中的示例客户端提供了一种查看各种其他响应代码的方法。我在这里使用了"alert"消息,因为服务器速度太快,以至于更新DOM会让它有时间完成,所以你永远无法看到中间代码。但是,"警报"在收到每个代码并等待你点击

"确定"后暂停,这样你就有机会让每个代码都能通过,这样你就可以看到它们(并在看到每个代码后点击确定) 。这里的服务器没有改变,它与清单6.2中的相同。

LISTING 6.3 Simple XMLHttpRequest Client Illustrates Response Codes

<html>

<body>

<h1 id="myHeader"> This is an AJAX example</h1>

<p> This code describes how to do an AJAX example </p>

<p id="thispart"> This is the part that is updated </p>

<p> It cleverly updates only part of a web page </p>

<form action="simple_server.php">

    <p> Enter your name: <input type="text" name="myname"/>

        <button type="button" onclick="doAJAXstuff(this.form)">

            Paint part of the web page        </button>

    </p>

</form>

<script type="text/javascript">

    function doAJAXstuff(theform) {

        request = new XMLHttpRequest();

        request.open("GET", "simple_server.php?mydata=" + theform.myname.value);

        request.onreadystatechange = function () {

            alert("testit data returned is :\n" + request.readyState);

        }

        request.send();

    }

 

</script>

</body>

不同状态下的响应代码如图6.36.5所示。

6.3  请求服务器接收到响应代码

6.4  请求服务器接收到响应代码

LISTING 6.4 PHP Server Used to Illustrate Asynchronous Operation

<?php

$mydata = $_REQUEST["mydata"];

sleep(5);

print "<p>Hi, $mydata! This is new important data for your web page.</p>";

?>

6.5  请求服务器接收到响应代码

在清单6.5中,我们看到异步XMLHttpRequest客户机。这个客户和之前的客户最重要的区别在于:

request.open("GET","simple_server.php?mydata="+theform.myname.value, true);

请注意,在此语句中,我们通过将异步参数设置为"true",将XMLHttpRequest的处理显式设置为异步。

LISTING 6.5 Asynchronous Client Using XMLHttpRequest

<html>
<body>
<h1 id="myHeader"> This is an AJAX example</h1>
<p> This code describes how to do an AJAX example </p>
<p id="thispart"> This is the part that is updated </p>
<p id="thatpart"> It cleverly updates only part of a web page </p>
<form action="simple_server.php">
    <p> Enter your name: <input type="text" name="myname"/>
        <button type="button" onclick="doAJAXstuff(this.form)">
            Paint part of the web page
        </button>
    </p>
</form>
<script type="text/javascript">
    function doAJAXstuff(theform) {
        request = new XMLHttpRequest();
        request.open("GET", "simple_server.php?mydata=" + theform.myname.value,
            true);
        request.onreadystatechange = function () {
            if (request.readyState == 4)
                my_callback();
        }
        request.send();
        document.getElementById("thatpart").style.font = "bold 30px Times " +
            "New Roman sans-serif";
    }

    function my_callback() {
        if (request.status == 200) {
            document.getElementById("thispart").innerHTML = request.responseText;
            document.getElementById("thispart").style.font = "italic bold 20px " +
                "arial, serif";
            alert("There wasn’t an error, just wanted to let you see how an alert " +
                "works. Received from the server:\n" + request.responseText);
        } else if (request.status = 404) {
            alert("Server was not found: data returned is :\n" + request.responseText);
        }
    }
</script>
</body>
</html>

运行此客户端生成的第一个Web页面视图如图6.6所示。请注意,这与前面图6.1中显示的相同。

现在,我们点击"Paint part of the web Page"按钮后查看此示例中发生的情况。在此示例中,我们在网页的不同部分使用字体,粗体和斜体来表示某些类型的处理正在发生。如图6.7 所示。

在图6.7中,我们看到该行的字体,字体大小和粗体已被更改:

"It cleverly updates only part of a web page."

回顾清单6.5,您可以看到这是由以下行完成的:

document.getElementById("thatpart").style.font = "bold 30px Times New Roman sans-serif";

6.6  由异步客户端生成的第一个web页面。

6.7  客户异步处理

6.8  客户端异步处理

此行表示客户端在调用XMLHttpRequest后继续处理时所执行的工作。也就是说,客户端在服务器单独进行处理时继续自己的工作。这是异步操作的一个例子。

替代方案是客户端在等待服务器完成时阻止并且不执行任何操作。(这个替代方案将是同步处理的一个例子。)

现在我们将看一下图6.8,它显示了在服务器结束处理后服务器调用回调函数my_callback(参见清单6.5)之后网页发生了什么当你运行这个示例时,你会发现我们不得不等待一段时间才能弹出提示界面(5秒钟,因为服务器睡了5秒钟),然后客户端页面局部刷新,这使我们有时间在最终业务处理结果返回之前服务器后台处理(见图6.7)—查看客户完成的中间工作。

总而言之,异步处理和同步处理之间的区别是:

 在同步处理中,客户端调用服务器然后阻塞并等待,直到服务器响应。

 在异步处理中,客户端调用服务器但客户端继续执行后续操作直到服务器响应为止。

 

SIDEBAR 6.1 跨域请求

如果您想运行代码访问和客户端不在同域上的服务器,那么使用XMLHttpRequest可能会遇到问题,这种使用脚本访问不同域的请求称为跨域请求。从一个域到另一个域的JavaScript调用容易被黑客攻击利用,所以浏览器通常不允许这样做。

如果您需要这样做,有三种常见方式:

 Web代理,您使用XMLHttpRequest访问本地Web服务器(代理服务器),代理服务器访问远程网站—一般较古老的浏览器必须这么处理

 使用JSONP(带填充的JavaScript)—如果您使用的是旧浏览器,建议使用本方法调用跨域请求

 跨源资源共享(CORS) - 在大多数现代Web浏览器中都支持

 

SIDEBAR 6.2 JSONP

JSONP利用了这样一个事实:当您在JavaScript中使用"script"标记时,脚本标记不受同源策略的约束。

首先,我们回忆HTML脚本标记中的"src"属性的作用。例如:

<script src=URL>

...

</script>

这里"src"属性可以访问不同的网站,也可以访问当前网站上的JavaScript文件,例如mywebsite/myscript.js

接下来,我们假设远程网站通常会传回JSON类型的数据(这在AJAX中很常见)

您要做的(因为我们使用的是JSONP)是让远程web站点将其JSON数据封装在一个对您自己定义的本地回调函数的调用中。

<script>

function myCallbackFunction(JSON text) {

...

}

</script>

<script src="url of server?jsoncallback=myCallbackFunction>

</script>

所以当"src"属性调用服务器时,它将返回如下内容:

myCallbackFunction({ "item": "value", "item2:"value2" })

请记住,JSON格式在语法上与JavaScript对象相同,但JSON仅为文本。您可以使用JavaScript函数JSON.parse(JSON文本)JSON转换为JavaScript对象。所以myCallbackFunction中的代码看起来像:

var myobj = JSON.parse(text);

所以在客户端你需要:

 定义好的回调函数

 调用<script>标记上的"src"属性指定的远程URL,按惯例,它应包含回调函数的名称

服务器将JavaScript对象文本的格式返回一个JavaScript函数调用。因此,当"src"属性执行脚本时,它将调用本地回调函数。

 

SIDEBAR 6.3 资源共享(CORS)

跨源资源共享标准提供了一种允许从一个域HTTP请求到另一个域的方法通常适用于RESTful类型的HTTP web服务,但是,这个标准特别用于允许域之间使用xmlhttprequest。我们将在CORS中使用XMLHttpRequest研究两种常见的场景:

 一个简单的跨站点请求

 使用GETHEADPOST(Content-Typetext/plainmultipart/form-dataapplication/x-www-formPOST)

 HTTP请求中没有自定义标头

 通常,一个简单的跨站点请求应该包含HTTP请求中的凭据

 预检请求预先发出的请求是客户端向服务器请求请求执行特定跨域请求的权限

 使用GETHEAD以外的方法或特定的POST(如果是POST动作,其Content-Typetext/plainmultipart/form-dataapplication/x-www-form的之外的类型)

 HTTP请求中有自定义标头

 例如,预检请求可能具有带有Content-Typeapplication/xmlPOST动作;另一个例子,它可以使用PUT

为了执行跨站点请求,CORSHTTP数据包中使用各种标头。使用的一些标头(以及其他几个标头)是:

 请求标头包括:

 Origin

 Access-Control-Request-Method

 响应标头包括:

 Access-Control-Allow-Origin

 Access-Control-Allow-Methods

要使XMLHttpRequest跨站点工作,浏览器必须支持使用CORSXMLHttpRequest

在简单的跨站点请求中,可以使用XMLHttpRequest发送简单的HTTP GET请求。但是, HTTP GET请求将包含列出当前网站的Origin标头:

Origin: www.myWebsite.com

如果服务器希望允许访问,那么服务器(让我们假设它在www.theRemoteWebsite.com)可以使用标头发送HTTP响应:

Access-Control-Allow-Origin: www.myWebsitecom

服务器也可以回复:

Access-Control-Allow-Origin: *

这意味着远程网站上的内容可供世界上任何人免费使用。

当客户端必须与服务器进行首次HTTP交换信息时,就会发送预检请求以查看是否允许发送实际HTTP请求。

这里,预检HTTP请求(让我们假设您最终要发送的实际请求是带有Content-Typeapplication / xmlPOST)将包含标头:

Origin: www.myWebsite.com

Access-Control-Request-Method

预检HTTP响应将包含:

Access-Control-Allow-Origin: http://mywebsite.com

Access-Control-Allow-Methods: POST, GET

在预检HTTP响应中接收到权限后,客户端可以发送真实的HTTP请求。

6.4 技术回顾JAVA Servlet

Java Servlet是一种向服务器添加动态内容的方法。这里的动态内容意味着Servlet在运行时更改用户看到的从服务器返回的内容以及服务器的行为方式。

使用了两个主要的包:javax.Servletjavax.Servlet.http。如果您打算使用HTTP协议(几乎每个人都使用),那么您可以让自己的Servlet代码javax.Servlet.http继承

Enterprise Java Bean(EJB)类似,Servlet必须在容器中运行,通常称为Servlet容器或Web容器。一个众所周知的开源Servlet容器的是Apache Tomcat。另一个开源示例是Tomcat我们将使用Apache Tomcat作为运行容器,当然您也可以选择使用TomcatTomcat仅仅Java Servlets的参考实现,Tomcat使用范围比TomcatTomcat不但提供了各种类型的容器,还有提供了一个Java EE应用程序服务器。TomcatTomcat实现都使用Catalina Servlet容器。其他支持Java Servlet的流行开源Web服务器还有:Apache TomEE(这是一个Java EE应用服务器)Wildfly(以前称为JBoss)

6.4.1 Servlet RESTFUL

ServletsRESTful架构还可以用作RESTful Web服务的开发?答案是:它们可能是,它部分取决于Servlets的实现方式或者可以说它取决于你对"RESTful"的定义有多宽松

Representational State Transfer(REST)架构风格是针对于分布式超媒体系统定义的。超媒体系统是一种音频视频图形文本和超文本组合的系统。例如,万维网是超媒体系统的主要示例。

如果您从第6.1节回忆起,RESTful架构风格背后的主要思想是它将具有以下约束:

•客户机/服务器:关注点分离,以客户机-服务器体系结构为例。其思想是不同的组件可以独立地发展—客户机中的用户界面可以独立于服务器发展,服务器也更简单。

•无状态:客户机-服务器交互是无状态的。服务器上没有存储上下文。任何会话信息都必须由客户机保存。

Cacheable:响应(对之前请求的响应)中的数据被标记为cache-ablenon-cache-able。如果它是可缓存的,客户端(或中介)将来可能会重用它来处理相同类型的请求。

•统一接口:组件之间有统一的接口。在实践中,有四个接口约束:

资源标识请求标识它们正在操作的资源(例如,通过URI)

通过资源表示进行资源操作当客户端或服务器访问资源时,基于对资源表示的理解,它有足够的信息来修改该资源

消息是自描述的消息包含足够的信息,允许客户端或服务器处理消息,这通常通过使用Internet媒体类型(MIME类型)来完成

使用超媒体改变应用程序的状态例如,服务器提供超链接,客户端使用超链接进行状态转换

分层系统:组件被组织在分层的层中,组件只知道交互发生的层。因此,连接到服务器的客户机不知道任何中间连接。

中间过滤器组件可以在消息传输过程中更改消息:因为消息是自描述的,而且语义是可见的,所以过滤器组件对消息有足够的了解,可以对其进行修改

代码随需应变:缺省支持代码随需应变,即客户端可以下载扩展其功能的脚本。

因此,要将Web服务的Servlet实现视为RESTful,它必须满足RESTful架构标准。

Servlet必须遵守客户端/服务器范例。情况显然如此。

此外,为了满足RESTful的严格要求,Servlet还必须以无状态方式运行,这取决于Servlet的编写方式。学习JavaEE的时候曾经讨论了如何将会话状态(以及可能的其他类型的状态信息)存储在Servlet中,由于可以在服务器端保存状态,因此以这种方式实现的服务器将不符合RESTful的严格定义。

关于统一接口约束,扩展HttpServlet接口(扩展javax.Servlet.http中定义的接口)ServletRESTful,因为它具有每个HTTP方法的处理程序,并且因为在请求和响应中发送的数据可以包含Internet媒体类型(MIME类型)数据:

doGet (HttpServletRequest request, HttpServletResponse response)

doPost (HttpServletRequest request, HttpServletResponse response)

doPut (HttpServletRequest request, HttpServletResponse response)

doDelete (HttpServletRequest request, HttpServletResponse response)

doHead (HttpServletRequest request, HttpServletResponse response)

doTrace (HttpServletRequest request, HttpServletResponse response)

doOptions (HttpServletRequest request, HttpServletResponse response)

同样关于统一接口,Servlet必须控制资源(通常通过URI访问)由于Servlet可以实现在持久存储(资源的一个例子)上使用的创建、读取、更新、删除(CRUD)操作,所以这通常控制资源很容易做到。

Servlet满足层次结构层约束和中间过滤器约束,因为它们使用HTTP协议进行通信,HTTP运行在TCP/IP之上标准Internet连接。

但是,Servlets通常被认为是RESTful中间件的汇编语言,也就是说,与其他一些RESTful中间件相比,它们的级别非常低。

6.4.2 Java Servlet的生命周期

处理HTTP请求的典型Servlet的生命周期方法在以下接口中定义的

javax.Servlet.Servlet

javax.Servlet.GenericServlet

javax.Servlet.http.HttpServlet

Servlet容器使用Servlet API中的以下方法控制Servlet生命周期:

Void init (ServletConfig config)

 Servlet上调用init()并向Servlet传递配置信息

Void service (ServletRequest request, ServletResponse response)

Void destroy()

 (还有一个名为getServletInfo的方法,Servlet可以使用它来提供有关自身的信息)

Servlet容器首先初始化Servlet

 URL映射到Servlet容器中的Servlet

 实例化Servlet

 Servlet上调用init()并向Servlet传递配置信息

初始化完成后,然后:

 Servlet容器收到客户端请求时,Servlet容器调用服务方法:

 service方法定义为javax.Servlet.GenericServlet中的抽象方法,并javax.Servlet.http.HttpServlet中重写

 服务方法产生一个新线程:

 ServletRequest对象被传递到Servlet

 Servlet接收ServletResponse对象,Servlet容器将该对象中的信息返回给客户端

 然后,服务方法调用适当的HTTP请求处理程序(javax.Servlet.http.HttpServlet指定):

 DoGet (HttpServletRequest request, HttpServletResponse response)

 DoPost (HttpServletRequest request, HttpServletResponse response)

DoPut (HttpServletRequest request, HttpServletResponse response)

DoDelete (HttpServletRequest request, HttpServletResponse response)

DoHead (HttpServletRequest request, HttpServletResponse response)

DoTrace (HttpServletRequest request, HttpServletResponse response)

DoOptions (HttpServletRequest request, HttpServletResponse response)

 (或者,可以直接调用各种处理程序,而不是使用服务方法)

当要从Servlet容器中删除Servlet实例时,Servlet容器会调用Servlet上的destroy()

 这允许Servlet在终止之前释放资源

 Servlet容器调用destroy的典型时间是:

 Servlet容器即将关闭时

 Servlet超时时

 Servlet容器需要Servlet占用的内存时

6.4.3 使用WEBServlet简单的Servlet示例

在本节中,我们将看一个非常简单的Servlet示例。此示例(如清单6.6所示)仅提供"doGet"处理程序这个Servlet扩展了HttpServlet,这就是为什么它可以使用一个doGet处理程序来接收一个包含HTTP Request作为输入的对象(HttpServletRequest对象),并返回一个包含HTTP响应作为输出的对象(HttpServletResponse对象)的原因

这是一个非常简单的Servlet,它在HTTP响应对象中返回一个小小的网页,其中包含以下数据作为有效负载:

<html><body><i>Happily working</i><b> myServlet</b> using @WebServlet</body></html>

请注意,此示例在发送响应之前将媒体类型(通常称为MIME类型)设置为"text/html"。我们将在接下来的几节中对媒体类型做一些改进。如果您还不了解媒体类型,则应在继续之前阅读第4.6节。

请注意语句的输出:

System.out.println("Inside myServlet in doGet method");

将显示在tomcat服务器日志文件中。

这就是这个Servlet实际上做的所有事情。现在让我们看一下如何从容器部署和访问Servlet

此示例使用@WebServlet注解告诉容器如何访问此Servlet。它为它提供了一个由"/myServlet"组成的URL模式(有一种不同的方法,称为web.xml文件,我们稍后会看到它)@WebServlet注解是在Servlet 3.0引入的(2009年发布),之前必须使用web.xml文件。

部署和运行它的方法如下:

首先,将Servlet编译为类文件,并将类文件放在目录"classes"中,该目录是另一个名为"WEB-INF"的目录的子目录WEB-INF是容器查找Servlet信息的服务目录

在这个极小的例子中,包名称按如下格式命名

package example.myServlet;

并且知道文件名与类名相同,即:

myServlet

那么WEB-INF目录将包含以下内容:

/WEB-INF/classes/example/myServlet/myServlet.class

目录"example/myServlet"由包名称example.myServlet设置,而文件名myServlet.class来自Java类的名称。

我们可以有一个不同的包名。例如,如果我们使用"anexample/someserv-let"作为包名(同时保持类名相同),那么WEB-INF将包含以下内容:

/WEB-INF/classes/anexample/someServlet/myServlet.class

但是我们没有这样做,所以我们将使用WEB-INF目录中的早期目录结构。

要部署Servlet,我们必须将它放在.war文件(Web应用程序ARchive文件)中。Jar文件和war文件都是使用Java jar存档工具创建的压缩文件。最常见的情况是在war文件中有一个WEB-INF目录。

要创建war文件,我们会这样做:

jar -cvf myServlet.war WEB-INF

请注意,我们再次使用"myServlet"名称作为war文件的名称(注意如果使用其他开发工具和包管理工具,注意打包的名字就是你应用的名字)。这很重要,因为它将成为我们用来访问此ServletURL的一部分。

如何在Tomcat部署.jar文件,请参阅tomcat使用指南。要部署.war文件,请执行相同的过程,只需部署.war文件而不是.jar文件。

Tomcat上部署myServlet.war文件后,我们现在可以使用以下URLWeb浏览器访问Servlet

http://localhost:8080/myServlet/myServlet

在这个URL中,第一个"myServlet"来自war文件的名称:该术语用作URL的基本部分,即"上下文根"。第二个"/myServlet"来自@WebServlet注解。是的,我可能应该使用不同的名称来区分这些,所以你可以弄清楚当你做这些变化时要使用哪些,但是为了非常简单的例子而想起名字会很无聊。我希望我已经解释得很好了。如果没有,没办法,请再次阅读一遍,或者从教科书中运行示例并更改内容,直到理解它为止。无论如何,这样做是最好的学习方式。

package example.myServlet.myServlet;

import java.io.*;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

@WebServlet(

        urlPatterns = "/myServlet"

)

public class myServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        // because System.out is standard output it will show up

        // on the console

        System.out.println("Inside myServlet in doGet method");

        // put a text/html header in the HTTP

        response.setContentType("text/html");

        // Write a small simple web page into the HTTP response

        PrintWriter myout = response.getWriter();

        myout.println("<html><body><i>Happily working</i><b>"

                + " myServlet</b> using @WebServlet</body></html>");

    }

}

6.2.4 如何将参数传递给WEB服务器(仍然使用WEBServlet)

在这个例子中,我们将看到如何将参数传递给Web Servlet。我们首先只传递一个参数这是一个字符串值如清单6.7所示。

这里我们检索数据参数如下:

String mystring;

//HTTP post中获取数据项,名称为:

mystring= request.getParameter("mystring");

我们将它写入Response如下:

//为响应准备一个writer

PrintWriter myout = response.getWriter();

//将刚刚收到的数据写入响应

myout.println("received data is "+mystring);

LISTING 6.7 Pass One String Parameter to a doPost

package example.myServlet;

import java.io.*;

import java.util.*;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

@WebServlet(

        urlPatterns = "/myServletx")

public class myServletx extends HttpServlet {

    @Override

    public void doPost(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doPost method");

        String mystring;

        // Get the data items from the HTTP post, by name

        mystring = request.getParameter("mystring");

        // Prepare a writer to the response

        PrintWriter myout = response.getWriter();

        // Write the data you just received to the response

        myout.println("received data is " + mystring);

    }

}

备注:注意Servlet的名称就是类名,如果放在同一个包重要唯一,这个好像不用提示。

要调用这个ServletdoPost通过浏览器的地址栏是简单的URL访问是完成不了的,您可以使用html表单,或者可以使用某些JavaScript或类似的机制来实现这中调用,类似于我们之前在AJAX中看到的。但是,在文中的示例中,我采用了不同的方式,我使用了cURL命令之前在4.10节讨论cURL,您可能还记得,cURL是一个命令行实用程序,可以发送各种协议,包括HTTP

我使用了以下cURL命令:

curl --request POST http://localhost:8080/myServlet/myServletx --data 'mystring=点击我'

现在让我们看看如何发送多个参数。看一下代码清单6.8。这里我们读取如下参数:

String mystring;

//HTTP Post请求中获取数据项,名称为

mystring = request.getParameter("mystring");

int myint;

//通过名称从HTTP POST中获取数据项

myint = Integer.parseInt(request.getParameter("myint"))

它与我们在前面的例子中的表现非常相似,只是这次我们必须将"myint"参数转换为整数。

非常类似于前面的示例我们将它写入Response

//为响应准备一个writer

PrintWriter myout = response.getWriter();

//将刚刚收到的数据写入Response

myout.println("收到的数据是"+ mystring +""+ myint");

LISTING 6.8 Pass One String Parameter and One Int parameter to a doPost

package example.myServlet;

import java.io.*;

import java.util.*;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

@WebServlet(

        urlPatterns = "/myServletm"

)

public class myServletm extends HttpServlet {

    @Override

    public void doPost(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doPost method");

        String mystring;

        // Get the data items from the HTTP post, by name

        mystring = request.getParameter("mystring");

        int myint;

        // Get the data items from the HTTP post, by name

        myint = Integer.parseInt(request.getParameter("myint"));

        // Prepare a writer to the response

        PrintWriter myout = response.getWriter();

        // Write the data you just received to the response

        myout.println("received data is " + mystring + " and " + myint);

    }

}

我们可以使用两个cURL命令来发送这两个参数。第一个类似于我们之前的参数传递示例:

curl --request POST http://localhost:8080/myServlet/myServletm --data 'mystring=hithere&myint=105'

但是,另一个有点不同,看起来很像cURL GET(当然我们正在使用POST)

curl --request POST "http://localhost:8080/myServlet/myServlet?mystring=hithere&myint=105"

6.2.5 使用WEBServlet稍微复杂的Servlet示例

在清单6.9中,我们将可以看到Servlet可以执行一些其他操作。首先,在6.2.2节中我们讨论Servlet生命周期的同时,我们讨论了"服务"方法。在代码清单6.9中,您可以看到"service"方法,但注解。如果这被注解掉,则各种处理程序例程(doGetdoPut)直接调用。但是,如果取消注解service例程,它将在调用HTTP动作处理之前调用他,并通过他做进一步的HTTP的动作调用一个围绕HTTP请求/响应处理添加其他处理过程的机会

另请注意,除了service之外,WEBSEVLET还有其他各种处理例程:

Init

Destroy

GetServiceInfo

我们之前在第6.2.2节中也看到了这一点。

System.out.println的各种调用结果被写入服务器日志,因此我们可以在服务器日志中跟踪运行时发生的情况。

当然WEBServlet还有几个处理方法:doGetdoPostdoPutdoDelete

本例doGet只提供一个简单的网页(text/html)输出,它不会读取任何输入参数。它与我们之前在清单6.6中看到的doGet非常相似。

doPost使用以下代码从HTTP请求获取名称的各种参数:

data1=request.getParameter("data1");

data2=request.getParameter("data2");

data3=request.getParameter("data3");

data4=request.getParameter("data4");

在以下cURL命令中,我们看到一个POST,其数据项名为:data1data2data3data4

curl --request POST http://localhost:8080/myServlet/myServletpm --data 'data1=hithere&data2=student&data3=from&data4=yourteacher'

部署此Servlet后,可以调用上面的命令并获得以下响应:

data1 is hithere

data2 is student

data3 is from

data4 is yourteacher

doPut使用以下代码读取在HTTP请求中发送的文件:

//HTTP PUT请求中获取文件数据

InputStream filedata = request.getInputStream();

然后它将此文件中的数据回送给客户端。

您可以使用cURL命令发送一个Put动作,该命令将名为"readme.txt"的文件发送到服务器。

curl -T readme.txt http://localhost:8080/myServlet/myServletpm

回显的输出只是readme.txt文件的内容。它有点长而且无聊,所以我不会在这里展示它的样子

其他方法如:initdestroygetServletInfodoDelete,在这些方法处理逻辑中我仅使用了System.out.println处理过程记录Tomcat服务器日志中getServletInfodoDelete处理中还向客户机返回一条简短的消息。

列表6.9使用WebServlet实现稍微复杂的Servlet

package example.myServlet;

import java.io.*;

import java.util.*;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

@WebServlet(

        urlPatterns = "/myServletpm"

)

public class myServletpm extends HttpServlet {

    @Override

    public void init(ServletConfig config)

            throws ServletException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in init method");

    }

    //commented service out to let the specific routines (doGet, etc.)

    // * handle the HTTP messages.

    @Override

    public void service(HttpServletRequest request,

            HttpServletResponse response) throws ServletException, IOException { //

        //Prepare a writer to the response

        PrintWriter myout = response.getWriter();

        String current_request = request.getMethod();

        myout.println("current request method is " + current_request); // Find out

        //which HTTP method is being called

        if (current_request.equals("GET")) {

            doGet(request, response);

        } else if (current_request.equals("POST")) {

            doPost(request, response);

        } else if (current_request.equals("PUT")) {

            doPut(request, response);

        } else if (current_request.equals("DELETE")) {

            doDelete(request, response);

        } else {

            myout.println("oops didn't recognize the request");

        }

    }

 

    @Override

    public void doGet(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // put a text/html header in the HTTP response

        response.setContentType("text/html");

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doGet method");

        // Write a small simple web page into the HTTP response

        PrintWriter myout = response.getWriter();

        myout.println("<html><body><i>Happily working</i><b> myServletpm</b> 使用 @WebServlet注解</body></html>");

    }

    @Override

    public void doPost(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doPut method");

        String data1, data2, data3, data4;

        // Get the data items from the HTTP post, by name

        data1 = request.getParameter("data1");

        data2 = request.getParameter("data2");

        data3 = request.getParameter("data3");

        data4 = request.getParameter("data4");

        // Prepare a writer to the response

        PrintWriter myout = response.getWriter();

        // Write the data you just received to the response

        myout.println("data1 is " + data1);

        myout.println("data2 is " + data2);

        myout.println("data3 is " + data3);

        myout.println("data4 is " + data4);

    }

    @Override

    public void doPut(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doPost method");

        // Prepare a writer to the response

        PrintWriter myout = response.getWriter();

        // Get the file data from the HTTP PUT request

        InputStream filedata = request.getInputStream();

        // Write the length of the file to the response

        myout.println("content length is " + request.getContentLength());

        // Write the files data back to the response

        myout.println("File data received by Servlet is : ");

        int data;

        data = filedata.read();

        while (data != -1) {

            myout.print((char) data);

            data = filedata.read();

        }

        myout.println();

    }

    @Override

    public void doDelete(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        // because System.out is standard output it will show up

        // on the console

        System.out.println("Inside myServlet in doDelete method");

        // Write to the response

        PrintWriter myout = response.getWriter();

        myout.println("I got the delete but I refuse to delete anything :-)");

    }

    @Override

    public String getServletInfo() {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in getServletInfo method");

        return "slightly_more_complicated_Servlet_by_Etzkorn";

    }

    @Override

    public void destroy() {

        // Following output will show up in Tomcat log file

        // because System.out is standard output it will show up

        // on the console

        System.out.println("Inside myServlet in destroy method");

    }

}

让我们看看Tomcat服务器日志,看看使用System.out.println调用放在那里的各种消息。在这里我们看到服务器"myServlet"的创建位置。然后我们看到从容器调用"init",调用"doGet",调用"doPost",调用doPut,调用doDelete。见图6.9

 

 
 

6.9 Tomcat服务器在执行了几个cURL命令后的日志记录。

6.2.6 使用WEB.XML的简单Servlet

如果你看一下清单6.10Servlet代码,它与清单6.6中的Servlet之间的唯一区别就是没有使用@WebServlet注解。相反,它使用了一个名为"web.xml"的单独文件用作部署描述正如您在第6.3.3节中回忆的那样,注解@WebServlet的引入的早版本是Servlet 3.0(2009年发布),之前必须使用web.xml文件部署Web应用使用@WebServlet注解的最大好处是,使用该注解,您的Java类名不在必须和web.xml文件中的类名和路径名匹配。使用注解也更容易,因为所有内容都在一个文件中,您不必在Java文件和web.xml文件之间来回切换。

但是,使用web.xml也有其有点,比如:如果您希望改变路由的时候不重新编译Java类,则必须使用web.xml

LISTING 6.10 Servlet Implemented Using web.xml

package example.myServlet;

import java.io.*;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

public class myServlet extends HttpServlet {

    public void doGet(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // put a text/html header in the HTTP response

        //response.setContentType("text/html");

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doGet method");

        // Write a small simple web page into the HTTP response

        PrintWriter myout = response.getWriter();

        myout.println("<html><body><i>Happily working</i><b> myServlet</b> "

                + "using web.xml file</body></html>");

    }

}

在清单6.11中,我们看到了一个web.xml部署描述,我们可以使用它来部署清单6.10中的Servlet。此web.xml文件将放在WEB-INF目录中,并像以前一样创建和部署war文件。

此特定web.xml文件的前几行显示它使用Servlet 3.1作为Java EE的一部分:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee

http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"

version="3.1">

接下来的几行给出了Servlet的名称以及实现该Servlet的类的全名:

<Servlet>

<Servlet-name>myServlet</Servlet-name>

<Servlet-class>example.myServlet.myServlet</Servlet-class>

</Servlet>

最后几行提供了与特定Servlet名称一起使用的url模式:

<Servlet-mapping>

<Servlet-name>myServlet</Servlet-name>

<url-pattern>/myServlet</url-pattern>

</Servlet-mapping>

与前面的示例一样,war文件的名称是URL的基本部分(上下文根)URL模式紧跟war文件名后面。和以前一样,它将在浏览器中访问,如下所示:

http://localhost:8080/myServlet/myServlet

LISTING 6.11 web.xml Deployment Descriptor

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee"

         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <Servlet>

        <Servlet-name>myServlet</Servlet-name>

        <Servlet-class>example.myServlet.myServlet</Servlet-class>

    </Servlet>

    <Servlet-mapping>

        <Servlet-name>myServlet</Servlet-name>

        <url-pattern>/myServlet</url-pattern>

    </Servlet-mapping>

    <session-config>

        <session-timeout>

            30

        </session-timeout>

    </session-config>

</web-app>

6.2.7 异步Servlet

我们在清单6.12中看到的Servlet示例说明了Servlet如何处理异步通信。在这种情况下,Servlet启动一个单独的线程运行(实现Runnable接口的doWork类是线程)。我假设你至少知道一些线程知识,我想尽可能多地解释,如果要是这么做,这本教科书将会偏离本书的主题。

Servlet中的原始进程使用以下命令触发doWork线程:

new Thread(new doWork(ac)).start();

Servlet中的原始进程触发doWork线程之前,它会设置一个异步侦听器。此侦听器正在等待doWork线程中发生的各种事件:

 OnComplete

 OnTimeout

 OnStartAsync

 OnError

然而,在这个特殊的例子中,惟一可能发生的事件是onComplete事件。如果您查看doWork线程,它会休眠20秒来模拟做一些实际的工作,然后它会创建一个HTTP响应,其中包含一个字符串,表示它一直在做一些非常有用的事情。然后这个过程完成,如下:

ac.complete(); //当工作完成时,表示完成

清单6.12异步Servlet

package example.myServlet;

import java.io.*;

import java.lang.Thread;

import javax.Servlet.annotation.WebServlet;

import javax.Servlet.*;

import javax.Servlet.http.*;

import javax.Servlet.http.HttpServlet;

import javax.Servlet.http.HttpServletRequest;

import javax.Servlet.http.HttpServletResponse;

import javax.Servlet.AsyncContext;

import javax.Servlet.AsyncEvent;

import javax.Servlet.AsyncListener;

import javax.Servlet.ServletException;

import javax.Servlet.http.HttpServlet;

class doWork implements Runnable {

    private AsyncContext ac;

    public doWork(AsyncContext the_ac) {

    // set the context of the thread to the original context

        this.ac = the_ac;

    }

 

    public void run() {

        System.out.println("inside doWork thread");

        // do a lot of hard work

        try {

            Thread.sleep(10000); // sleep for 10 sec to simulate hard work

            // Put some stuff in the response so it get done after completion

            PrintWriter myout = ac.getResponse().getWriter();

            myout.println("this is really useful stuff");

            System.out.println("just wrote some stuff into response");

        } catch (Exception e) {

            System.out.println("Some kind of Exception");

        }

        ac.complete();

        // when hard work is done, signify completion

    }

}

 

@WebServlet(

        urlPatterns = "/myServletaw", asyncSupported = true

)

public class myServletaw extends HttpServlet {

 

    public void doGet(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException {

        // Following output will show up in Tomcat log file

        System.out.println("Inside myServlet in doGet method");

        AsyncContext ac = request.startAsync();

        ac.addListener(new AsyncListener() {

            @Override

            public void onComplete(AsyncEvent event) throws IOException {

                System.out.println("onComplete: Asynchronous task is now done");

            }

 

            @Override

            public void onTimeout(AsyncEvent event) throws IOException {

                System.out.println("onTimeout");

            }

 

            @Override

            public void onStartAsync(AsyncEvent event) throws IOException {

                System.out.println("onStartAsync");

            }

 

            @Override

            public void onError(AsyncEvent event) throws IOException {

                System.out.println("onError");

            }

        }

        );

        System.out.println("just before thread start");

        new Thread(new doWork(ac)).start();

        System.out.println("just after thread start");

        System.out.println("very end of doGet");

    }

}

不同的System.out.println语句写入Tomcat系统日志此异步Servlet运行的系统日志如图6.10所示。

请注意, 您还可以使用 web.xml 文件来部署异步Servlet。为此,您只需删除@WebServlet注解,并将清单6.11中的web.xml文件添加到WEB-INF目录中,并为web.xml文件添加一个标记以使其异步:

<async-supported>true</async-supported>

本教科书附带的代码中提供了使用web.xml文件的具有此异步Servlet的单独示例

6.10 异步Servlet的服务器日志。

6.2.8 HTTP请求/响应调用和服务器会话之间的服务器存储

在本节中,我们将讨论如何将数据存储在Servlet使用此信息,您可以创建一个可用于将不同HTTP请求/响应调用相互关联的会话您应该记住,RESTful架构包括无状态作为其标准之一。因此,在Servlet中使用这样的会话意味着您没有最严格定义的RESTful架构。Servlet有一个称为属性的方便的东西,可以很容易地将资源存储在Servlet(从而在容器内部)(属性也可用于在Servlet之间传递信息。)

Java EE Web应用程序中的属性和参数与Servlet作用域相关,该作用域指定该属性或参数可供使用的范围Servlet的作用域是Servlet维护其状态的范围。

要在其作用域内使用属性信息,可以调用这些方法

 SetAttribute(name, object)保存对象及其名称

 GetAttribute(name)返回与指定名称关联的对象

 RemoveAttribute(name)从上下文中删除对象

 GetAttributeNames()返回包含所有属性名称的枚举

 可以这么做

Enumeration the_names = request.getAttributeNames();

Servlet有三个作用域

  1. 应用程序(上下文)作用域—Web应用程序的生命周期。当Web应用程序从开始到结束(或重新加载)整个生命周期过程中,此作用域中的数据可用它在所有用户与特定Web应用程序的交互期间有效。
  2. 请求作用域—请求的持续时间。从Servlet收到HTTP请求到Servlet生成HTTP响应时,此作用域中的数据有效在单个HTTP请求期间,单个用户与Web应用程序交互时有效。
  3. 会话作用域—客户端的生命周期。从客户端与应用程序建立连接到连接关闭期间,此范围内的数据都是有效的它在来自单个用户的多个HTTP请求之间是有效的。

Application(Context)作用域使用javax.Servlet.ServletContext接口,该接口定义Servlet用于与Servlet容器通信的方法。Java虚拟机中的每个Web应用程序都有一个上下文要访问它,您可以执行以下操作:

ServletContext mycontext = getServletContext();

或者

ServletConfig myconfig = getServletConfig();

  ServletContext mycontext=myconfig.getServletContext();

请求作用域使用javax.Servlet.http.HttpServletRequest接口,该接口向HTTP Servlet提供HTTP请求。使用此作用域时,Servlet容器会创建HttpServletRequest对象,并将其传递给Servlet中的doGetdoPut动作,要访问此作用域,可以传递给doGet的请求对象(来自doGet()内部)调用getAttribute(name)方法

会话作用域使用javax.Servlet.http.HttpSession接口用于创建从HTTP客户端(一个用户)HTTP服务器持续指定时间段的会话要使用此作用域,您可以调用;

HttpSession session = request.getSession();

GetSession() - 获取会话,如果还没有HttpSession对象,则创建一个。

getSession例程的工作方式如下:

如果启用了cookie

• 它生成一个JSESSIONID cookie和值,用于在来自该客户机的进一步请求到达时标识此会话。此JSESSIONID cookie在响应中发送给客户机,客户机在未来的请求中使用它。

HTTP标头(如果显示)将包括:

Set-CookieJSESSIONID = ..长杂乱的十六进制值......;Path=/example; Expires=.. 日期..;安全

如果未启用cookie,则使用URL重写:

URL的末尾添加了JSESSIONID,并将请求重定向到此URL

www.mstuff.com/hellowww.mstuff.com/hello/JSESS

IONID=凌乱的十六进制值......

URL重写包括告诉服务器将请求从一个URL重定向到另一个URL。通常,URL重写是为了让客户机首先访问更具可读性的URL,然后将URL重定向到可读性较差的内部URL(这两种情况下的域名是相同的,但是URL的其余部分需要重定向)。然而,在这种情况下,URL重写用于建立会话,因此,可读的URL被重新路由到较不可读的URL

6.3 通过Spring REST API构建Restful服务

用于RESTful Web服务的Spring REST API也是一种基于Java的中间件技术,它用于创建RESTful Web服务,即实现遵循RESTful架构所期望的特性的Web服务。请回顾我们6.1节讨论RESTful架构和RESTful Web服务的含义。

简单地说,RESTful体系结构由定义良好以无状态方式访问和操作的资源组成。万维网被认为是RESTful架构的一个不错的例子在万维网的体系中每个资源使用URL表示,并且访问这些资源的操作通过HTTP协议来实现

Spring REST API提供了一种使用Java语言实现通过HTTP协议操纵和访问web上的资源的方法,这些资源的位置由URL指定,并且Spring Restful以无状态的方式访问

本节的示例代码采用Spring Boot2.5构建示例代码,Spring Boot通常通过内置 TomcatJettyUndertowNetty 等服务器容器,当我们添加了 spring-boot-starter-web 依赖后,默认会使用 Tomcat 作为 Web 容器,当然也可以是遵循Servlet 3.X/4.X规范的其他容器。当然,还可以使用其他方法构建和部署Spring Restful,例如使用EJB容器(可能是为EJB 提供RESTful 接口) OSGi 实现(一个OSGi实现是Apache Felix)本小节基于内置的Tomcat实现Spring Restful服务,示例均假设部署在默认的Tomcat容器中

6.3.1 Spring REST API基础Restful相关注解

Spring REST API使用Java类定义上的注解来定义资源,并定义可以在这些资源上执行的相关操作。Spring认为是"resource"类的类是可以通过URL访问的类。使用类似于清单6.13中的注解来指定资源类(注解以粗体显示)

LISTING 6.13 JAX-RS Resource Class

@RestController

public class Hello {

@GetMapping("/")

public String method_that_handles_GETs() {

...useful stuff...

}

}

清单6.13中的类是一个资源类,因为它可以通过URL访问,因为它要么提供某种有用的数据,要么提供某种有用的功能("功能"意味着它做某种工作),通过使用资源方法注解(HTTP消息关联的方法注解)的方法来访问这些功能。

注意,Spring Restful中的根资源类的默认生命周期是每个请求。这意味着,每当HTTP请求中的URI路径匹配资源时,就会创建根资源类的新实例因此,Spring Restful是无状态的。

此特定示例提供了@GetMapping资源方法注解,关联的方法method_that_handles_GETs()称为"资源方法"。当Servlet容器(EJB容器或OSGi shell)收到HTTP GET消息时,将调用此方法。对于本章的其余讨论,我们将假设在Servlet容器中进行部署通常,资源方法注解指示当Servlet容器收到HTTP请求时调用哪些资源方法。

这些资源方法注解指示HTTP命令对应的访问方法最基本的资源方法注解如下(稍后会看到其他几个)

  1. @GetMapping-处理HTTP GET请求,
  2. @PostMapping-处理HTTP POST请求
  3. @PutMapping-处理HTTP PUT请求
  4. @DeleteMapping-处理HTTP Delete请求
  5. @PatchMapping-处理HTTP PATCH 请求,一般用于更新资源
  6. @RequestMapping-通用请求处理,可以通过@RequestMapping(value = "资源路径",method = RequestMethod.POST)来处理对应HTTP请求,这里是POST,其他的分别是PUTGETDELETE来处理对应请求

这些基本资源方法注解中的每一个都可能有伴随参数被发送到带注解的处理程序方法上,特别是当您发送POSTPUT时,通常使用一些参数是必须的

GET上,并不一定有参数,但发送一个参数很常见,尽管这通常是一个好的处理方法GET一起发送的查询字符串参数通常会被记录在许多位置(比如web服务器日志、浏览器中的历史缓存等等),因此存在安全风险。查询字符串参数被附加到URL之后,如下面的示例所示:

http://example.com/mystuff?paramA=2000¶mB=3000

还有其他注解来指定资源方法的参数如何绑定到HTTP请求中对应的参数,这些注解称为"参数注解"。这是一种"注入"形式您可能还记得第2.2.2节讨论的依赖注入。基本上"注入"的意思是从它来自哪里获取您需要的特定信息并将其填充到您需要的地方。

一些最基本的参数注解是:

  1. @PathVariable可以处理指定URL中包含的一个或多个参数,例如:my.example.com/mypath/myparametervalue
  2. @RequestParam可以处理指定查询字符串格式的一个或多个参数例如myexample.com?myparametername=myparametervalue
  3. @RequestParam也可以处理指定urlencoded格式的一个或多个参数(application/x-www-form-urlencoded),例如从HTML表单发送时会出现的参数
  4. @RequestBody—接收的参数是来自requestBody中,即请求体。一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:application/jsonapplication/xml等类型的数据。就application/json类型的数据而言,使用注解@RequestBody可以将body里面所有的json数据传到后端,后端再进行解析。

在本章的后面部分,我们将介绍如何使用这些基于参数的注解

请注意,这两个都需要在关联的方法上使用对应的路由注解

您还可以使用这些基本方法注解上不同的关键字接收和发送不同的Internet媒体类型(MIME类型)它们可以指定方法在调用时期望接收的Internet媒体类型,在指定的路由注解上使用以下关键字:

consumes

这些还可以使用以下注解指定您的方法将生成Internet媒体类型并在响应中发回:

produces

有关Internet媒体类型的说明,请参见第4.6节。接受特定类型的Internet媒体类型,您使用类似于清单6.14中所示的注解方法(Internet媒体类型相关联的注解以粗体显示)

LISTING 6.14 Simple Internet Media Type (MIME type) Annotation Example

@PostMapping(path = "/hello", consumes= MediaType.TEXT_XML_VALUE)

public class Hello {

public String method_that_handles_POSTs() {

...useful stuff...

}

}

请注意,Internet媒体类型可用于区分所调用的方法。请参见清单6.15如果接收到HTTP_POST,其中包含一个头文件,该头文件指定HTTP请求(Content-Type:text/xml)中包含具有互联网媒体类型text/xml的数据,那么将调用名为"method_that_handles_POSTs_for_TEXT_XML()"的方法,然而,如果HTTP POST包含一个头,该头指定HTTP请求中包含具有Internet媒体类型text/plain的数据(Content-Type: text/plain),那么将调用名为"method_that_handles_POSTs_for_TEXT_PLAIN()"的方法。

LISTING 6.15 Simple Internet Media Type (MIME Type) Annotation Example

@RequestMapping("/hello")

public class Hello {

@RequestMapping(method = RequestMethod.POST,consumes= MediaType.TEXT_XML_VALUE)

public String method_that_handles_POSTs_for_TEXT_XML() {

...useful stuff...

}

@RequestMapping(method = RequestMethod.POST,consumes= MediaType.TEXT_PLAIN_VALUE)

public String method_that_handles_POSTs_for_TEXT_PLAIN() {

...useful stuff...

}

}

如果您的代码使用produces关键字,它的工作方式类似考虑一种情况,您在资源类中的资源方法上使用了以下注解

@PostMapping(path = "/hello", consumes= MediaType.TEXT_XML_VALUE)

或者

@RequestMapping(path = "/hello",method=RequestMethod.POST,consumes=MediaType.TEXT_PLAIN_VALUE)

然后,如果收到带有标头(Accept:text/plain)HTTP POST请求,指定客户端希望接收包含具有Internet媒体类型text/plain的数据的响应,然后将调用此资源方法。请注意,带有Accept:text/xml标头的HTTP POST请求不会导致调用此资源方法—必须使用另一种方法必须处理这个请求。

我们将在本章稍后讨论有关如何通过Spring处理Internet媒体类型的更多信息。

要对Spring REST API有一个相当广泛的了解,还需要了解一些其他定义

 @CookieValue-从与cookie相关的HTTP头中提取信息。

 @MatrixVariable-URL路径段中提取信息

URL Matrix参数具有以下形式:

http://myexample.com/mystuff;paramA=2000;paramB=3000

而查询参数(如前所述)具有以下形式:

http://myexample.com/mystuff?paramA=2000¶mB=3000

但是,查询参数和矩阵参数之间的主要区别在于查询参数会作为一个整体随着请求发送出去,而矩阵参数作为请求中的特定路径元素。查询参数的示例可能是:

http://myexample.com/mystuff?studentname=Smith&professorname=Etzkorn

而等效矩阵参数可能是:

http://myexample.com/mystuff/students;name=Smith/professors;name=Etzkorn

6.3.2 简单的Spring REST示例

我们现在来看几个Spring REST示例。在本节中,我们首先看一个处理GETPOSTPUT的简单示例,区分不同的互联网媒体类型(MIME类型),包括产生什么样的数据(Internet媒体类型/MIME类型),以及使用什么数据。

正如您将从第6.3.1看到的那样,"produces"是指资源方法将在响应中发回的数据类型(Internet媒体类型)。路由注解中关键字produces指定资源方法生成的Internet媒体类型HTTP请求中的头Accept: internet媒体类型(示例:text/plain)用于将HTTP请求映射到适当的资源方法来处理它。

类似地,从第6.3.1节开始,"consumes"是指资源方法期望在请求中接收的数据类型(Internet Media Type)路由注解中关键字consumes指定资源方法期望接收的Internet媒体类型,HTTP请求中的内容类型:互联网媒体类型(例如:text/plain)用于将HTTP请求映射到适当的资源方法来处理它。

在此示例中,我们使用各种cURL命令发送不同的HTTP消息和Internet媒体类型(MIME类型),以检验清单6.16中提供的不同资源方法。

LISTING 6.16 Simple JAX-RS Example Showing Produces and Consumes Internet Media Types (MIME Types)

package com.example.demo.controller;

 

import org.springframework.web.bind.annotation.RestController;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.PutMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestParam;

 

@RestController

public class TestHello {

@GetMapping(value = "/hello", produces = MediaType.TEXT_PLAIN_VALUE)

public String return_plaintext() {

return "Hello World!\n";

}

 

@GetMapping(value = "/hello", produces = MediaType.TEXT_HTML_VALUE)

public String return_html() {

return "<html> " + "<body>" + "<h1>" + "Hello" + "</h1>" + "<h2>" + "to" + "</h2>" + "<h2>" + "all the World"

+ "</h2>" + "</body>" + "</html> " + "\n";

}

 

@GetMapping(value = "/hello", produces = MediaType.TEXT_XML_VALUE)

public String return_XML() {

return "<?xml version=\"1.0\"?>" + "<thedata> Hello World" + "</thedata>" + "\n";

}

 

@PostMapping(value = "/hello")

public String received_POST(@RequestParam("mystuff") String msg) {

return "POST: got a string " + msg + "\n";

}

 

@PutMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE)

public String received_JSON_PUT(@RequestBody String msg) {

return "PUT: got a JSON file " + msg + "\n";

}

 

@PostMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<String> received_JSON_POST(@RequestBody String msg) {

if (!msg.isEmpty()) {

String mymessage = "POST: got a JSON file " + msg + "\n";

return new ResponseEntity<>(mymessage, HttpStatus.OK);

} else {

return new ResponseEntity<>("Null String Received\n\n", HttpStatus.NOT_FOUND);

}

}

}

我们先来看看以下资源方法:

@GetMapping(value = "/hello", produces = MediaType.TEXT_PLAIN_VALUE)

public String return_plaintext() 

它由清单6.17中所示的cURL命令调用,它产生的输出也如代码清单6.17所示。看到标头"Accept:text/plain"映射到注解的关键字:

produces = MediaType.TEXT_PLAIN_VALUE

LISTING 6.17 cURL Command GET Accept Media Type text/plain (Maps to Produces text/plain)

curl --request GET -H "Accept: text/plain" http://localhost:8080/hello

Output produced is:

Hello World

接下来让我们看看以下资源方法:

@GetMapping(value = "/hello", produces = MediaType.TEXT_HTML_VALUE)

public String return_html()

它由清单6.18中所示的cURL命令调用,它产生的输出也如代码清单6.18所示。标头Accept:text/html映射到注解的关键字:

produces = MediaType.TEXT_HTML_VALUE

LISTING 6.18 cURL Command GET Accept Media Type text/html (Maps to Produces text/html)

curl --request GET -H "Accept: text/html" http://localhost:8080/hello

Output produced is:

<html> <body><h1>Hello</h1><h2>to</h2><h2>all the World</h2></body></html>

接下来让我们看看以下资源方法:

@GetMapping(value = "/hello", produces = MediaType.TEXT_XML_VALUE)

public String return_XML() {

它由清单6.19中所示的cURL命令调用,它产生的输出也如清单6.19所示。标头Accept:text/xml映射到注解的关键字:

produces = MediaType.TEXT_XML_VALUE

LISTING 6.19 cURL Command GET Accept Media Type text/xml (Maps to Produces text/xml)

curl --request GET -H "Accept: text/xml" http://localhost:8080/hello

Output produced is:

<?xml version ="1.0"?><thedata> Hello World</thedata>

接下来让我们看看以下资源方法:

@PostMapping(value = "/hello")

public String received_POST(@RequestParam("mystuff") String msg) {

它由清单6.20中所示的cURL命令调用,它产生的输出也如代码清单6.20所示。

LISTING 6.20 cURL Command POST Default myvariable=value Format Uses Content-Type application/x-www-form-urlencoded (Maps to POST with no MediaType Annotation)

curl -X POST --data "mystuff=32" http://localhost:8080/hello

Output produced is:

POST: got a string mystuff=32

请注意,当您使用带有"-data"参数并且未指定媒体类型cURL命令cURL会以与使用HTML表单并按下"提交"按钮时相同的方式发送HTTP请求中的数据。包括数据作为有效负载,并包括(默认情况下)内容类型"application/x-www-form-urlencoded"但是,当资源方法上没有MediaType注解时,它处理此请求。

接下来让我们看看以下资源方法:

@PutMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE)

public String received_JSON_PUT(@RequestBody String msg)

它由清单6.21中所示的cURL命令调用,它产生的输出也如清单6.21所示,假设cURL命令中指定的mydata文件是清单6.22中所示的mydata文件,标头Content-Type:application/json映射到注解

@Consumes(MediaType.APPLICATION_JSON)

LISTING 6.21 cURL Command PUT Uses Content-Type application/json (Maps to Consumes application/json)

curl -X PUT -H "Content-Type:application/json" -d @mydata.json http://localhost:8080/hello

Output produced is:

PUT: got a JSON file { "firstName":"Joe", "lastName":"Blow",

"education":{"bachelors":"BSCS","masters":"MSCS"}, "employer":"ACME Software" }

后,让我们看看这个例子中的最后一个资源方法:

@PostMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<String> received_JSON_POST(@RequestBody String msg) {

它由清单6.23中所示的cURL命令调用,它产生的输出也如代码清单6.23所示,假设cURL命令中指定的mydata文件是如清单6.22所示mydata.json文件这个cURL命令有点不同,因为它使用了"-i"参数。"-i"参数指定响应的HTTP标头将包含在输出中。

LISTING 6.22 Example mydata JSON File

{

"firstName":"Joe",

"lastName":"Blow",

"education":

{

"bachelors":"BSCS",

"masters":"MSCS"

},

"employer":"ACME Software"

}

请注意,输出响应标头显示"Content-Type:text/plain"这是因为此特定资源方法返回一个字符串。查看此资源方法产生响应的位置:

String mymessage = "POST: got a JSON file " + msg + "\n";

return new ResponseEntity<>(mymessage, HttpStatus.OK);

它专门构建了状态为200的响应正如您应该从4.2节中学习到的那样,HTTP状态代码200表示正常,请求已成功处理然后它创建了一个"实体"你应该记住第6.3.1节中一个实体封装了一个消息,还包括有关Internet媒体类型语言和编码的信息。这里封装的实体是一个字符串,因此使用的Internet媒体类型是text/plain

LISTING 6.23 cURL Command POST Uses Content-Type application/json (Maps to Consumes application/json)

curl -i -X POST -H "Content-Type: application/json" -d @mydata.json http://localhost:8080/hello

Output produced is:

 

HTTP/1.1 200

Content-Type: text/plain;charset=UTF-8

Content-Length: 147

Date: Mon, 20 Sep 2021 09:08:12 GMT

 

POST: got a JSON file { "firstName":"Joe",      "lastName":"Blow",      "education":    {"bachelors":"BSCS",             "masters":"MSCS"        },      "employer":"ACME Software"}

仍然看着最后一个资源方法,让我们看看当我们向它发送一个空文件时会发生什么这种情况如清单6.24所示,这个cURL命令产生的输出也如代码清单6.24所示清单6.24中的cURL命令与清单6.23中显示的cURL命令相同,只是文件名为"mydata1.json"而不是"mydata.json",在这种情况下,"mydata"文件是一个空文件,按照代码的设想应该是资源方法执行以下代码:

return new ResponseEntity<>("Null String Received\n\n", HttpStatus.NOT_FOUND);

但是这份代码会因为RequestBody收到一个NULL,引发Sping的错误保护机制:

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity<java.lang.String> com.example.demo.controller.TestHello.received_JSON_POST(java.lang.String)]

如清单6.24所示,响应头中包含HTTP 400状态代码这个状态代码,表示Bad Request。不会激活路由代码,Spring会自动创建一个系统的错误信息返回去

LISTING 6.24 cURL Command POST Uses Content-Type application/json (Maps to Consumes application/json)

curl -i -X POST -H "Content-Type: application/json" -d @mydata1.json http://localhost:8080/hello

Output produced is:

 

HTTP/1.1 400

Content-Type: application/json

Transfer-Encoding: chunked

Date: Mon, 20 Sep 2021 09:50:04 GMT

Connection: close

 

{"timestamp":"2021-09-20T09:50:04.679+00:00","status":400,"error":"Bad Request","path":"/hello"}

这个简单的例子为您提供了开始使用Spring REST所需的大部分内容。但是,还有一些其他的东西,如何如何传递复杂参数,如何发布应用,我们将在以下部分中查看这些问题。

6.3.3 将参数传递给Spring REST服务器

您应该记得我们之前在6.3.1节中讨论过的"参数注解"。在Spring REST中,参数注解用于指定资源方法的参数如何绑定到HTTP请求中的参数。

这是一种"注入"形式你可能还记得2.2节中关于依赖注入的讨论。本质上讲,"注入"表示的是从它来自哪里获取您需要的特定信息并将其填充到您需要的地方。

一些最基本的参数注解是:

  1. @PathVariable 指定URL中包含的一个或多个参数,绑定路径中的占位符参数到方法参数变量中;只能绑定路径中的占位符参数,且路径中必须有参数。无论是 GET 或者POST 只要 URL中有参数即可!例如:my.example.com/mypath/myparametervalue
  2. @RequestParam --指定URL中包含一个或多个的参数或者多个urlencoded格式的参数 (application/x-www-form-urlencoded),例如从HTML表单发送时会出现的参数。
  1. 常用来处理简单类型的绑定,通过Request.getParameter() 获取的String可直接转换为简单类型的情况( String--> 简单类型的转换操作由ConversionService配置的转换器来完成);因为使用request.getParameter()方式获取参数,所以可以处理get 方式中queryString的值,也可以处理post方式中 body data的值;
  2. 用来处理Content-Type: application/x-www-form-urlencoded编码的内容,提交方式GETPOST
  3. 该注解有两个属性: valuerequiredvalue用来指定要传入值的id名称,required用来指示参数是否必须绑定;
  4. 例如: myexample.com?myparametername=myparametervalue
  1. @MatrixVariable --根据 URI 规范 RFC 3986 URL 的定义,路径片段中可以包含键值对,规范中并没有对应的专门术语,尽管独特的 矩阵 URI” 也经常被使用并且相当有名,但还是一般 “URL 路径参数来等同视之。在 Spring中给了一个独特的名称:矩阵变量,矩阵变量可以出现在任何路径片段中,每一个矩阵变量都用分号(;)隔开。比如 “/cars;color=red;year=2012”;多个值可以用逗号隔开,比如 “color=red,green,blue”,或者分开写 “color=red;color=green;color=blue”。如果你希望一个 URL 包含矩阵变量,那么请求映射模式必须用URI模板来表示这些矩阵变量。这样的话,不管矩阵变量顺序如何,都能够保证请求可以正确的匹配。

在本节中,我们使用这些参数注解练习各种示例。

6.3.3.1 @PathVariable参数注解

在本节中,我们将查看作为URL的一部分传递的参数。这些被称为"路径参数",因为它们是作为构成URL的路径的一部分传递的。

第一个简单例子如代码清单6.26所示我们将一个字符串参数传递给POST请求的处理函数。

在这个例子中,我们看到URL路径本身已经扩展,使用变量作为扩展。这样做如下:

在本节中,我们将查看作为URL的一部分传递的参数。这些被称为"路径参数",因为它们是作为构成URL的路径的一部分传递的。

第一个简单例子如代码清单6.26所示我们将一个字符串参数传递给POST请求的处理函数。

在这个例子中,我们看到URL路径本身已经扩展,使用变量作为扩展。这样做如下:

@PostMapping(value = "/phello/{mystring}")

LISTING 6.26 Simple Path Parameter Example Using POST

package com.example.demo.controller;

 

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

 

@RestController

public class ParaController {

    @PostMapping(value = "/phello/{mystring}")

    public String postMethodName(@PathVariable("mystring") String myString) {

        System.out.println("Data received was " + myString + "\n");

        return "data received was " + myString + "\n";

}

}

这里的"mystring"是变量的名称。你知道,因为它用大括号括起来。如果它没有用大括号括起来,那么"mystring"只是一个字符串,它是URL的一部分。

确定mystring变量的数据类型,可以查看resource方法的参数列表:

@PathVariable("mystring") String myString

调用代码清单6.26中代码的基本方法是清单6.27中显示的cURL命令及其输出。在此,基本路径是:

http://localhost:8080/myrestful/restful/hello

"hithere"是发送到mystuff的字符串值。

LISTING 6.27 cURL Command for Simple Path Parameter Example

curl --request POST http://localhost:8080/phello/hithere

Output produced is:

data received was hithere

对于这个特定的例子,将数据作为-data"hithere"发送的cURL命令也可以工作,但是以这种方式调用它不是路径参数应该工作的方式。

接下来让我们看一个例子,如代码清单6.28所示,我们将多个路径参数发送到资源方法。和以前一样,@PostMapping语句定义变量:

@PostMapping(value = "/phello/{mystring}/{myint}")

在这种情况下,有两个变量,名为"mystring""myint"。与这两个变量关联的数据类型显示在参数列表中:

@PathVariable("mystring") String mystring

@PathVariable("myint") int myint

这表明变量"mystring"的类型为string,变量"myint"的类型为integer

LISTING 6.28 Path Parameter Example with Multiple Parameters Using POST

    @PostMapping(value = "/phello/{mystring}/{myint}")

    public String received_POST(@PathVariable("mystring") String mystring, @PathVariable("myint") int myint) {

        System.out.println("Data string received was " + mystring + "\n");

        System.out.println("Data int received was " + myint + "\n");

        return "data received was: " + mystring + " and " + myint + "\n";

    }

调用代码清单6.28中代码的基本方法是清单6.29中显示的cURL命令及其输出。

LISTING 6.29 cURL Command for Path Parameter Example with Multiple Parameters

curl --request POST http://localhost:8080/phello/hithere/105

Output produced is:

data received was: hithere and 105

6.3.3.2 @RequestParam参数注解

在本节中,我们将介绍使用查询字符串一个或多个参数传递给Spring REST服务器我们之前在第6..2节中讨论了查询字符串形式参数典型的查询字符串参数将附加到URL之后,如以下所示(此示例显示两个查询字符串参数)

http://example.com/mystuff?paramA=2000¶mB=3000

人们倾向于认为查询字符串形式参数是作为HTTP GET请求的一部分发送的,但它们也可以通过POSTPUT发送清单6.30中的示例Spring REST服务器期望收到一个附带字符串查询参数POST请求。

在此示例中,参数列表中的@RequestParam注解确定查询参数如何解析

@RequestParam("mystring") String mystring

请注意,与我们在上一节中看到的路径参数示例不同,@PostMapping(value = "/postpath")符号实际上与查询参数无关,它只是整个URL路径的一部分。

LISTING 6.30 Simple Query Parameter Example Using POST

    @PostMapping(value = "/postpath")

    public String received_POST_postpath(@RequestParam("mystring") String mystring) {

        System.out.println("Data我说的服received was " + mystring + "\n");

        return "data received was " + mystring + "\n";

    }

调用代码清单6.30中代码的基本方法是cURL命令,如清单6.31所示,以及它的输出。

LISTING 6.31 cURL Command for Simple Query Parameter Example

curl --request POST http://localhost:8080/postpath?mystring=hithere

Output produced is:

Data我说的服received was hithere

我们在代码清单6.32中看到的下一个示例使用HTTP POST请求将多个查询参数传递给JAX-RS服务器。

在此示例中,两个查询参数按如下方式传递,如资源方法的参数列表中所示:

(@RequestParam("mystring") String mystring, @RequestParam("myint") String myint)

这里我们传递一个名为"mystring"的查询参数,该参数具有字符串值,另一个名为"myint"的查询参数具有整数值。

LISTING 6.32 Query Parameter Example with Multiple Parameters Using POST

    @PostMapping(value = "/postpatha")

    public String received_POST(@RequestParam("mystring") String mystring, @RequestParam("myint") String myint) {

        System.out.println("Data string received was " + mystring + "\n");

        System.out.println("Data int received was " + myint + "\n");

        return "data received was " + mystring + " and " + myint + "\n";

    }

}

调用代码清单6.32中代码的基本方法是使用以下cURL命令,包括多个查询参数:

curl --request POST http://localhost:8080/postpath?mystring=hithere\&myint=107

cURL命令生成的输出是:

data received was hithere and 107

6.3.3.3 application/x-www-form-urlencoded类型数据处理

在本节中,我们讨论如何将参数urlencoded格式传递给JAX-RS服务器(application/x-www-form-urlencoded)就像HTML表单发送的数据一样

我们将在清单6.33中看到的示例JAX-RS服务器期望POST请求接收单个字符串表单参数。

在资源方法的参数列表中指定表单参数的方式如下

LISTING 6.33 Simple Form Parameter Example Using POST

package com.example.demo.controller;

 

import org.springframework.http.MediaType;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

 

@RestController

public class UrlcodeprocessController {

    @PostMapping(value ="/uhello",consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE)

    public String received_POST_fm(@RequestParam("mystring") String mystring) {

        System.out.println("Data received was " + mystring + "\n");

        return "data received was " + mystring + "\n";

    }

}

调用代码清单6.33中代码的基本方法是HTML表单,如代码清单6.34所示。

LISTING 6.34 HTML form for Simple Form Parameter Example

<html>

<body>

<h1 id="myHeader"> This is a JAX-RS example</h1>

<form action = "http://localhost:8080/myrestful/restful/hello" method="POST">

<p> Enter data to send: <input type="text" name="mystring" /> </p>

</form>

</body>

</html>

6.11显示了HTML表单生成的用户界面。当字符串"Letha"进入此表单并单击Submit按钮时,将生成清单6.35中所示的输出。

6.11 清单6.34所示HTML表单生成的用户界面

LISTING 6.35 Output Produced by Simple Form Parameter Example

data received was 这是测试数据

6.3.3.4 扩展参数示例路由注解中consumes/produces的使用

给出清单6.36中的示例有两个原因一个是让学生看到更多参数传递的例子。

LISTING 6.36 Need for consumes/produces When Using Parameters

package com.example.demo.controller;

 

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

 

@RestController

public class ConsumeProduceController {

@GetMapping(value = "/xhello")

public ResponseEntity<String> return_plaintext_no_params() {

return new ResponseEntity<>("GET no params \n\n", HttpStatus.OK);

}

 

@GetMapping(value = "/xhello", produces = MediaType.TEXT_HTML_VALUE)

public ResponseEntity<String> return_html_query_param(@RequestParam("id") int id) {

return new ResponseEntity<>("GET query parameter " + id + "\n\n", HttpStatus.OK);

}

 

@GetMapping(value = "/xhello", produces = MediaType.TEXT_XML_VALUE)

public ResponseEntity<String> return_html_query_param(@RequestParam("id") int id,

@RequestParam("thestuff") String thestuff) {

return new ResponseEntity<>(

"GET multiple query parameters: id is " + id + " and Thestuff is " + thestuff + "\n\n", HttpStatus.OK);

}

 

@GetMapping(value = "/xhello/{id}")

public ResponseEntity<String> return_plaintext_single_param(@PathVariable("id") String id) {

return new ResponseEntity<>("GET short path: value passed is" + id + "\n\n", HttpStatus.OK);

}

 

@GetMapping(value = "/xhello/more/{id}", produces = MediaType.TEXT_PLAIN_VALUE)

public ResponseEntity<String> return_plaintext_multiple_params(@PathVariable("id") String id) {

return new ResponseEntity<>("GET longer path: value passed is" + id + "\n\n", HttpStatus.OK);

}

 

@PostMapping(value = "/xhello/{id}")

public ResponseEntity<String> received_POST_single_param(@PathVariable("id") String id) {

return new ResponseEntity<>("POST: value passed is " + id + "\n\n", HttpStatus.OK);

}

 

@GetMapping(value = "/xhello/{id}/{thestuff}")

public ResponseEntity<String> return_plaintext_single_param(@PathVariable("id") int id,

@PathVariable("thestuff") String thestuff) {

return new ResponseEntity<>(

"GET multiple path parameters: id is " + id + " and thestuff is " + thestuff + "\n\n", HttpStatus.OK);

}

}

清单6.36中的示例的另一个目的是说明,通过注解中的关键字consumesproduces限制的每个资源方法关联的Internet媒体类型(MIME类型)来辅助Spring REST区分HTTP请求具体路由到哪个资源方法

实际上,如果未指定Internet媒体类型,Tomcat将不会加载从此示例创建的war文件。虽然这个文件有可能会在没有producesconsumes关键字的情况下编译成功,但在尝试加载文件时,Tomcat会产生一个错误,说它无法确定向哪个方法发送GET请求。以下内容请学生们自我练习发现问题:有选择地删除与GET方法相关的producesconsumes语句,并查看它无法编译或加载到服务器中的原因,主要是为了说明查询参数本身不同,不足以区分一种资源方法和另一种资源方法。

6.3.3.5 @MatrixVariable注解

根据 URI 规范 RFC 3986 URL 的定义,路径片段中可以包含键值对,规范中并没有对应的专门术语,尽管独特的 矩阵URI” 也经常被使用并且相当有名,但还是一般 “URL 路径参数来等同视之。在 Spring中给了一个独特的名称:矩阵变量,矩阵变量可以出现在任何路径片段中,每一个矩阵变量都用分号(;)隔开。比如 “/cars;color=red;year=2012”;多个值可以用逗号隔开,比如 “color=red,green,blue”,或者分开写 “color=red;color=green;color=blue”。如果你希望一个 URL 包含矩阵变量,那么请求映射模式必须用URI模板来表示这些矩阵变量。这样的话,不管矩阵变量顺序如何,都能够保证请求可以正确的匹配。虽然从Spring 3.2就已经支持@MatrixVariable特性,但直至现在其依然为默认禁用的状态,我们需要手工配置开启或者重写WebMvcConfigurer中的configurePathMatch方法才能使用。

假如现在需要设计一个用于搜索某部门某些员工可选信息中的部分信息API,我们分别使用查询字符串和路径name-value方式来设计对比,看看具体效果:

1、查询字符串方式:/api/v1/users/optional-info?dept=321&name=joh*&fields=hometown,birth

问题:其中的deptname理应属于users路径,而fields则属于optional-info路径,但现在全部都要挤在查询字符串中。

2、路径name-value方式:/api/v1/users/depts=321;name=joh*/optional-fields/fields=hometown,birth

可以看出路径name-value的方式逻辑上更在理些。

@MatrixVariable注解属性说明

在正式开始前我们先死记硬背一下注解的属性吧。

  • value 和属性pathVar的别名;
  • pathVar 用于指定name-value参数所在的路径片段名称
  • name 用于指定name-value参数的参数名
  • required 是否为必填值,默认为false
  • defaultValue 设置默认值

其中pathVarname到底是什么呢?请继续看后面的示例吧,准能秒懂!

LISTING 6.37 重写configurePathMatch开启@MatrixVariable

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;

import org.springframework.web.util.UrlPathHelper;

 

@Configuration

public class MatrixVariableConfig implements WebMvcConfigurer {

 

    @Override

    public void configurePathMatch(PathMatchConfigurer configurer) {

        UrlPathHelper urlPathHelper = new UrlPathHelper();

        urlPathHelper.setRemoveSemicolonContent(false);

        configurer.setUrlPathHelper(urlPathHelper);

    }

}

我们来看看@MatrixVariable注解的详细使用及使用过程中可能会遇到的问题,见列表6.38,通过curl模拟访问的命令,留给同学们做课后作业了。

LIST 6.38 @MatrixVariable使用示例

package com.example.demo.controller;

 

import org.springframework.web.bind.annotation.MatrixVariable;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

 

@RestController

public class MatrixVariableController {

参数仅有一个值的玩法

注意:多个name-value间以分号分隔,如name=joh*;dept=321

LIST 6.38 @MatrixVariable使用示例 ()

        /*

         * 1. 获取单个路径片段中的参数 请求URI/Demo2/66;color=red;year=2020

         */

        @RequestMapping(path = "/Demo1/{id}", method = RequestMethod.GET)

        public String test1(@PathVariable String id, @MatrixVariable String color, @MatrixVariable String year) {

                return year;

        }

 

        /*

         * 2. 获取单个路径片段中的参数 请求URI/Demo2/color=red;year=2020

         */

        // /Demo2/66;color=red;year=2020

        @RequestMapping(path = "/Demo2/{id}", method = RequestMethod.GET)

        public String test2(@MatrixVariable String color, @MatrixVariable String year, @PathVariable("id") String id) {

                return "@MatrixVariable获取name-value参数:\n\n" + "color=" + color + " \nyear=" + year

                                + "\n\n@PathVariable获取参数:\nid=" + id;

        }

        /*

         * 3. 获取不同路径片段中的参数 请求URI

         * /Demo3/66;color=red;year=2020/pets/77;color=blue;year=2019

         */

        @RequestMapping(path = "/Demo3/{id1}/pets/{id2}", method = RequestMethod.GET)

        public String test3(@PathVariable String id1, @PathVariable String id2,

                        @MatrixVariable(name = "color", pathVar = "id1") String color1,

                        @MatrixVariable(name = "year", pathVar = "id1") String year1,

                        @MatrixVariable(name = "color", pathVar = "id2") String color2,

                        @MatrixVariable(name = "year", pathVar = "id2") String year2) {

                return "\n\n@PathVariable获取参数:路径id1=" + id1 + "\n@MatrixVariable获取name-value参数 路径id1=" + id1 + "附加参数\n"

                                + "\ncolor1=" + color1 + "\nyear1=" + year1 + "\n\n@PathVariable获取参数:id2=" + id2

                                + "\n@MatrixVariable获取name-value参数,路径id2=" + id2 + "附加参数\n" + "\ncolor2=" + color2

                                + "\nyear2=" + year2;

        }

 

        /*

         * 4. 获取不同路径片段中的参数 请求URI

         * /Demo4/color=red;year=2020/pets/77;color=blue;year=2019

         */

        @RequestMapping(path = "/Demo4/{id1}/pets/{id2}", method = RequestMethod.GET)

        public String test4(@PathVariable String id1, @PathVariable String id2,

                        @MatrixVariable(name = "color", pathVar = "id1") String color1,

                        @MatrixVariable(name = "year", pathVar = "id1") String year1,

                        @MatrixVariable(name = "color", pathVar = "id2") String color2,

                        @MatrixVariable(name = "year", pathVar = "id2") String year2) {

                return "\n路径@PathVariable id1=" + id1 + "附加参数\n" + "\n@MatrixVariable获取name-value参数:" + "\nyear1="

                                + year1 + "\n路径@PathVariable id2=" + id2 + "附加参数\n" + "\n@MatrixVariable获取name-value参数:"

                                + "\ncolor2=" + color2 + "\nyear2=" + year2;

        }

        /*

         * 5. 通过Map获取所有或指定路径下的所有参数

         * /Demo5/color=red;year=2020/pets/77;color=blue;year=2019

         */

        @RequestMapping(path = "/Demo5/{id1}/pets/{id2}", method = RequestMethod.GET)

        public String test5(@PathVariable String id1, @PathVariable String id2, @MatrixVariable Map<String, Object> all,

                        @MatrixVariable(pathVar = "id1") Map<String, Object> mapId1) {

                return "\n路径@PathVariable id1=" + id1 + "附加参数\n@MatrixVariable map类型:" + mapId1.toString()

                                + "\n@MatrixVariable 获取全部name-value:" + all.toString();

        }

参数有多个值的使用方法法,若参数值不是单个,那么可以通过两种方式传递:

  1. 值之间通过逗号分隔,如dept=321,123
  2. 重名name-value对,如dept=321;dept=123

LIST 6.38 @MatrixVariable使用示例 ()

 

        /*

         * 请求为/Demo6/color=123,321 那么color值为123,321

         */

        @RequestMapping(path = "/Demo6/{id}", method = RequestMethod.GET)

        public String test6(@MatrixVariable Integer[] color) {

 

                return String.valueOf(color);

        }

 

        /*

         * 请求为/Demo7/color=123;color=321 那么color值为123,321

         */

        @RequestMapping(path = "/Demo7/{id}", method = RequestMethod.GET)

        public String test7(@MatrixVariable Integer[] color) {

                return String.valueOf(color);

        }

那些要注意到的问题,在参数多值的情况下还有如下3个要注意的问题,请各位多加注意:

  1. String参数类型可以接受通过逗号和通过重名name-value传递的所有值,而其它类型只能获取第一个值。

LIST 6.38 @MatrixVariable使用示例 ()

        /*

         * 请求为/Demo8/color=123,321 那么color值为123,321

         */

        @RequestMapping(path = "/Demo8/{id}", method = RequestMethod.GET)

        public String test8(@MatrixVariable String color) {

                return color;

        }

        /*

         * 请求为/Demo9/color=123;color=321 那么color值为123,321

         */

        @RequestMapping(path = "/Demo9/{id}", method = RequestMethod.GET)

        public String test9(@MatrixVariable String color) {

                return color;

        }

        /*

         * 请求为/Demo10/color=123;color=321 那么color值为123

         */

        @RequestMapping(path = "/Demo10/{id}", method = RequestMethod.GET)

        public String test10(@MatrixVariable Integer color) {

                return String.valueOf(color);

        }

  1. Map<String, Object[]>只能获取参数中的第一个值而已。

LIST 6.38 @MatrixVariable使用示例 ()

        /*

         * 请求为/Demo11/color=123,321 那么color值为123

         */

        @RequestMapping(path = "/Demo11/{id}", method = RequestMethod.GET)

        public String test11(@MatrixVariable Map<String, Integer[]> color) {

                return color.toString();

        }

  1. 不同路径片段中出现名称相同的参数,那么必须通过pathVar标识所有相同参数所属路径,否则URI匹配失败。

LIST 6.38 @MatrixVariable使用示例 ()

        // 以下handler仅标识第二个参数的pathVar,而没有标识第一个,那么也是会匹配失败的。

        // /Demo12/color=red;year=2020/pets/77;color=blue;year=2019

        @RequestMapping(path = "/Demo12/{id1}/pets/{id2}", method = RequestMethod.GET)

        public String test12(@MatrixVariable String color,

                        @MatrixVariable(name = "color", pathVar = "id2") String color2) {

                return color2;

        }

}

通过curl或者postman模拟访问,就留给同学的课后作业了。

6.4 Visual Studio Code课程相关配置

本课程使用Java语言,过去我在讲课的时候喜欢用EclipseNetBean,但随着时间的推移,这两样的工具正在逐渐的从行业消失,尤其是NetBean,而Visual Studio CodeJetbrainIDEA正在逐渐成为开发的主流的,所以我在进行一系列更替,IDEA收费,虽说教育版免费,但也不是每个学校都提供学生邮箱的,就是可以拿到教育授权,工作后呢?所以本课程开发的IDE选择Visual Studio Code,原因免费,而且可以在一个IDE中,通过装插件的方法,适应于不同语言应用的开发,本课程主要是Java,验证Ajax的时候会用到几行PHP,讲解HTTP协议的时候会用的一些Nodejsexpress,包管理工具maven,开发框架Spring Boot,验证Servlet的时候会用到Tomcat9.X,均可以无缝的在一个开发环境下写代码,下面我就讲解一下环境配置

6.4.1 Java环境的安装和配置

目前Java是每半年滚动一个版本,除了长期支持版,其他版本支持的期限都是半年,故我们选择最近的一个长期支持版JDK11,在写文档的时候最新的长期支持版JDK17出来了,观望一下,等下一次更新讲稿的时候,再更新。

  1. 下载

https://www.oracle.com/java/technologies/downloads/#java11

根据操作系统不同下载自己需要的版本,下载过程中会需要登陆,没账户的请注册一个即可。

Windows

 

 
 

MacOS

 

 
 

Linux

 

 
 
  1. 安装

Windows双击安装即可,安装过程中一定要记住java最终的安装目录,以当前的版本来讲应该安装在c:\program files\java\jdk-11.0.12目录,一定要看你下载的是什么版本的,这个目录和下载的版本有对应关系;LinuxMacOS先解压,均将解压后的目录重名为jdk11,然后将这个目录移动的你的个人目录的lib目录下,没有这个目录就建立一个,比如你登陆用户名是xuser,那么你儿个人目录应该是(可以通过Terminalpwd查看):

MacOS:/Users/xuser

Linux:/home/xuser

  1. 环境变量配置

Windows:此电脑(我的电脑或计算机)-右键-属性-高级系统设置-环境变量 在系统环境变量处看有没有如下三个变量:JAVA_HOMECLASSPATH,没有就创建,有就修改其内容为:

JAVA_HOMEc:\program files\java\jdk-11.0.12

CLASSPATH.;%JAVA_HOME%\lib

查看Path变量里有没有%JAVA_HOME%\bin,如果没有在其尾部添加志即可,使用分号;隔离。

linux:

使用编辑器,编辑/etc/profle,在尾部增加如下内容:

export MAVEN_HOME=/home/xuser/lib/maven

export JAVA_HOME=/home/xuser/lib/jdk11

export CLASSPATH=.:$JAVA_HOME/lib

export PATH=$JAVA_HOME/bin:$PATH:$MAVEN_HOME/bin:/home/xuser/lib/node/bin

macos:

使用编辑器,编辑/etc/profle,在尾部增加如下内容:

export MAVEN_HOME=/Users/xuser/lib/maven

export JAVA_HOME=/Users/xuser/lib/jdk11

export CLASSPATH=.:$JAVA_HOME/lib

export PATH=$JAVA_HOME/bin:$PATH:$MAVEN_HOME/bin:/Users/xuser/lib/node/bin

重启系统,打开Terminal输入:java -version验证,看到如下信息说明Java配置成功

java version "11.0.12" 2020-04-14 LTS

Java(TM) SE Runtime Environment 18.9 (build 11.0.12+8-LTS)

Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.12+8-LTS, mixed mode)

6.4.2 Visual Studio Code安装

1、下载,网站自动侦测你的系统

https://code.visualstudio.com/

根据你的系统点击剪头处下载,速度会很慢,点击浏览器黄色剪头位置,弹出菜单点击查看下载内容:

会看到如下画面:

黄色和红色剪头指向的位置右键:

点击红色剪头处的复制网址得到如下内容:

https://az764295.vo.msecnd.net/stable/83bd43bc519d15e50c4272c6cf5c1479df196a4d/code_1.60.1-1631294805_amd64.deb

标灰处用:vscode.cdn.azure.cn替换即可

最后下载网址为:

https://vscode.cdn.azure.cn/stable/83bd43bc519d15e50c4272c6cf5c1479df196a4d/code_1.60.1-1631294805_amd64.deb

复制到浏览器的地址栏,即可快速下载。

  1. 安装

Windows双击安装即可,注意在安装过程中要把添加卓面快捷方式的选项给勾上,linux右键安装,MACOS直接拖到应用程序即可

  1. 配置

运行vscode,安快捷键:ctrl+shift+x(mac:command+shift+x)

Java开发环境安装如下几个插件:

tomcat安装如下插件:

Spring安装如下插件:

 

写代码的时候为了方便可以安装如下辅助插件:

 

maven的配置:

首先在下载:https://mirrors.bfsu.edu.cn/apache/maven/maven-3/

选择北外镜像的原因,速度快,我选择的是3.6.3版本,你可以按照自己的习惯,

https://mirrors.bfsu.edu.cn/apache/maven/maven-3/3.6.3/binaries/

两个文件内容是一样的,我选择的是zip解压方便,下载后的后文件名称:

apache-maven-3.6.3-bin.zip

解压:

目录更名成maven

移动到你的个人目录的lib目录下面,Windows是你自己能很容易找到的地方,macos/Users/xuser/liblinux/home/xuser/lib下面。这是我的Linux下个人目录/home/oliver/lib/maven

maven的源默认是在国外,下载依赖库速度会很慢,按照我的方式修改,换成国内的阿里云的源

,这样下载速度会非常快,配置文件是/home/xuser/lib/maven/conf/settings.xml,其内容修改为:

<?xml version="1.0" encoding="UTF-8"?>

<!--

Licensed to the Apache Software Foundation (ASF) under one

or more contributor license agreements.  See the NOTICE file

distributed with this work for additional information

regarding copyright ownership.  The ASF licenses this file

to you under the Apache License, Version 2.0 (the

"License"); you may not use this file except in compliance

with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

...

-->

<!--

 | This is the configuration file for Maven. It can be specified at two levels:

 |

 |  1. User Level. This settings.xml file provides configuration for a single user,

 |                 and is normally provided in ${user.home}/.m2/settings.xml.

 |

 |                 NOTE: This location can be overridden with the CLI option:

 |

 |                 -s /path/to/user/settings.xml

 |

 |  2. Global Level. This settings.xml file provides configuration for all Maven

 |                 users on a machine (assuming they're all using the same Maven

 |                 installation). It's normally provided in

 |                 ${maven.conf}/settings.xml.

 |

 |                 NOTE: This location can be overridden with the CLI option:

 |

 |                 -gs /path/to/global/settings.xml

 |

 | The sections in this sample file are intended to give you a running start at

 | getting the most out of your Maven installation. Where appropriate, the default

 | values (values used when the setting is not specified) are provided.

 |

 |-->

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0

                      http://maven.apache.org/xsd/settings-1.0.0.xsd">

  <localRepository>/home/oliver/.m2/er</localRepository>

  <interactiveMode/>

  <usePluginRegistry/>

  <offline/>

  <pluginGroups/>

  <servers/>

  <mirrors>

    <mirror>

     <id>aliyunmaven</id>

     <mirrorOf>central</mirrorOf>

     <name>阿里云公共仓库</name>

     <url>https://maven.aliyun.com/repository/central</url>

    </mirror>

    <mirror>

      <id>repo1</id>

      <mirrorOf>central</mirrorOf>

      <name>central repo</name>

      <url>http://repo1.maven.org/maven2/</url>

    </mirror>

    <mirror>

     <id>aliyunmaven</id>

     <mirrorOf>apache snapshots</mirrorOf>

     <name>阿里云阿帕奇仓库</name>

     <url>https://maven.aliyun.com/repository/apache-snapshots</url>

    </mirror>

  </mirrors>

  <proxies/>

  <activeProfiles/>

  <profiles>

    <profile>  

        <repositories>

           <repository>

                <id>aliyunmaven</id>

                <name>aliyunmaven</name>

                <url>https://maven.aliyun.com/repository/public</url>

                <layout>default</layout>

                <releases>

                        <enabled>true</enabled>

                </releases>

                <snapshots>

                        <enabled>true</enabled>

                </snapshots>

            </repository>

            <repository>

                <id>MavenCentral</id>

                <url>http://repo1.maven.org/maven2/</url>

            </repository>

            <repository>

                <id>aliyunmavenApache</id>

                <url>https://maven.aliyun.com/repository/apache-snapshots</url>

            </repository>

        </repositories>             

     </profile>

  </profiles>

</settings>

内容重的注释主要是版权和文件格式介绍,如果你觉得无用可以删除,建议保留。

vscodemaven的配置:

按快捷键:Ctrl+,(MacOS:command+,),弹出如下画面

在搜索栏输入maven:

分别在黄色剪头指向的地方填写你的maven对应的信息,我这里是Linux,你要是Windows系统,注意目录就是maven详细的位置,不能照抄哦,这三个位置必须正确配置,maven才能正常使用。

Tomcat的配置

建议现在tomcat9.X,暂时不要要下载tomcat10,主要是javax包的更名问题,写代码的时候会麻烦,下一版本,我会更新成支持JAKARTA EE 9规范的,这里还是按照Java EE8来配置。

下载:https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-9/v9.0.53/bin/

根据自己的系统下载对应版本,

建议统一下载黄色箭头指向的那个,解压,重命名成tomcat,移动到自己可以精确知道的目录,windows建议和maven放在同一个目录下,macoslinux建议放在自己个人目录的lib目录。

配置VScode

点击红色剪头出的加号,会弹出一个定位tomcat所在目录的窗口,定位到你放置Tomcat的地方即可

点击打开出现如下画面说明就配置好了:

在红色剪头处右键:

选择start,查看输出窗口,看到如下画面,tomcat就启动成功了

通过浏览器访问:127.0.0.1:8080,就可以看到如下画面:

Tomcat发布项目

通过mavenpackage命令

然后就可以在项目的target目录看到打包好的war文件,在打包好的war文件上右键,选择Run on Tomcat Server,就会自动的发布到定义好的tomcat服务器上:

接着在Tomcat Servers就能看到你定义好的tomcat服务器下面就有了对应工程的war文件

浏览器打开127.0.0.1:8080就能看到你发布的应用了:

6.4.3 一个使用vscode构造的Spring Boot数据库的RestfulCRUD示例

无论你愿不愿意,SpringBoot已经是微服务开发的主流,在开发web项目的时候的方便、快捷超越了过去的大部分框架。很多同学一直以为StrutsSpringMVC都用得不太熟练,能学Spring Boot吗。其实Springboot并没有想象中的那么难,SSM并不是学习Springboot的前置知识,学完JavaJSPServlet就可以直接上手Spring Boot

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。

简单的来说就是,只需几个jar和一些简单的配置,就可以快速开发项目。假如我就想简单的开发一个对外的接口,那么只需要以下代码就可以了。

一个主程序启动springBoot

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

控制层

@RestController

public class HelloWorldController {

    @RequestMapping("/hello")

    public String index() {    

        return "Hello World";

    }

 }

成功启动主程序之后,编写控制层,然后在浏览器输入 http://localhost:8080/hello 便可以查看信息了。感觉使用SpringBoot开发程序是不是非常的简单呢!

SpringBoot实战的话来说:

这里没有配置,没有web.xml,没有构建说明,甚至没有应用服务器,但这就是整个应用程序了。SpringBoot会搞定执行应用程序所需的各种后勤工作,你只要搞定应用程序的代码就好。

下面我们用Springboot来构建一个简单的单表的数据库的CRUD后台,而且以restful方式实现。

1、创建工程(Ctrl+shift+P

 

  1. 选择:Spring Initializer:Create Maven Project ,选择2.5.4

  1. 选择语言Java

 

  1. 输入你包名:cn.edu.bbc.computer

  1. 输入应用的名称:restfulapp

  1. 选择应用打包格式:war

  1. 选择JDK版本:11

  1. 选择使用的包:SpringWeb WebMariaDB Driver SQLMybatis Framework SQL

  1. 选择工作区:这里我们桌面建立一个resful

  1. 点击打开:会自动生成一个项目

  1. 创建数据库和表

create database test

DROP TABLE IF EXISTS `Student`;  

CREATE TABLE `Student`  (

 `sclass` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 `ssex` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 `sdept` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 `sname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 `sage` int,

 `saddr` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 `sid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,

 PRIMARY KEY (`sid`) USING BTREE,

 INDEX `sid`(`sid`) USING BTREE

 ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci;

  1. 编辑resources/application.properties文件,其内容如下:

spring.datasource.url=jdbc:mariadb://localhost:3306/test?useUnicode=true&characterEncoding=utf8

spring.datasource.username=oliver

spring.datasource.password=123456

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

server.port=8080

 

spring.mvc.view.suffix=.html

spring.mvc.view.prefix=/

  1. 编写POJO

定义和数据表对应的实体类,这里根据数据库的数据表结构定义了Student类,代码如下:

package cn.edu.bbc.computer.restfulapp.pojo;

 

import java.util.*;

 

public class Student{

 

private String ssex;

private String sname;

private String saddr;

private String sclass;

private Integer sage;

private String sdept;

private String sid;

 

public String getSsex() {

return ssex;

}

public String getSname() {

return sname;

}

public String getSaddr() {

return saddr;

}

public String getSclass() {

return sclass;

}

public Integer getSage() {

return sage;

}

public String getSdept() {

return sdept;

}

public String getSid() {

return sid;

}

public void setSsex(String ssex) {

 this.ssex = ssex;

}

public void setSname(String sname) {

 this.sname = sname;

}

public void setSaddr(String saddr) {

 this.saddr = saddr;

}

public void setSclass(String sclass) {

 this.sclass = sclass;

}

public void setSage(Integer sage) {

 this.sage = sage;

}

public void setSdept(String sdept) {

 this.sdept = sdept;

}

public void setSid(String sid) {

 this.sid = sid;

}

public Student(){

super();

}

 

public Student(String ssex, String sname, String saddr, String sclass, Integer sage, String sdept, String sid){

super();

this.ssex = ssex;

this.sname = sname;

this.saddr = saddr;

this.sclass = sclass;

this.sage = sage;

this.sdept = sdept;

this.sid = sid;

}

@Override

public String toString() {

return "Student [ssex=" + ssex + ", sname=" + sname + ", saddr=" + saddr + ", sclass=" + sclass + ", sage=" + sage + ", sdept=" + sdept + ", sid=" + sid + "]";

}

 

}

14、编写dao

在以前的Dao层这块,hibernatemybatis 都可以使用注解或者使用mapper配置文件。在这里我们使用springJPA来完成基本的增删改查。

一般有两种方式实现与数据库实现CRUD
第一种是xmlmapper配置。
第二种是使用注解,@Insert@Select@Update@Delete 这些来完成。

这里我们使用的是第二种。

package cn.edu.bbc.computer.restfulapp.dao;

 

import java.util.List;

import cn.edu.bbc.computer.restfulapp.pojo.Student;

import org.apache.ibatis.annotations.Delete;

import org.apache.ibatis.annotations.Insert;

import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Param;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Update;

 

@Mapper

public interface StudentDao {

  /**

   * 学生数据新增

   */

  @Insert("insert into Student(sid,sname,ssex,sage,sclass,sdept,saddr) values (#{sid},#{sname},#{ssex},#{sage},#{sclass},#{sdept},#{saddr})")

  void addStudent(Student student);

 

  /**

   * 学生数据修改

   */

  @Update("update Student set sname=#{sname},ssex=#{ssex},sage=#{sage},sclass=#{sclass},sdept=#{sdept},saddr=#{saddr} where sid=#{sid}")

  void updateStudent(Student student);

 

  /**

   * 学生数据删除

   */

  @Delete("delete from Student where sid=#{sid}")

  void deleteStudent(int id);

 

  /**

   * 根据学生名称查询学生信息

   *

   */

  @Select("SELECT sid,sname,ssex,sage,sclass,sdept,saddr FROM Student where sname=#{sname}")

  Student findStudentByName(@Param("sname") String sname);

 

  /**

   * 查询所有

   */

  @Select("SELECT sid,sname,ssex,sage,sclass,sdept,saddr FROM Student")

  List<Student> findAll();

 

}

说明:

 

mapper : 在接口上添加了这个注解表示这个接口是基于注解实现的CRUD

Results: 返回的map结果集,property 表示User类的字段,column 表示对应数据库的字段。

Param:sql条件的字段。

InsertSelectUpdateDelete:对应数据库的增、查、改、删。

 

15、编写Service 业务逻辑层

这块和hibernatemybatis的基本一样。代码如下:

接口

package cn.edu.bbc.computer.restfulapp.service;

 

import java.util.List;

import cn.edu.bbc.computer.restfulapp.pojo.Student;

 

public interface StudentService {

     /**

 * 新增学生

 * @param student

 * @return

 */

boolean addStudent(Student student);

/**

 * 修改学生

 * @param student

 * @return

 */

boolean updateStudent(Student student);

/**

 * 删除学生

 * @param id

 * @return

 */

boolean deleteStudent(int id);

 

 /**

     * 根据学生名字查询学生信息

     * @param sname

     */

Student findStudentByName(String sname);

    

    /**

     * 查询所有

     * @return

     */

    List<Student> findAll();

}

实现类:

package cn.edu.bbc.computer.restfulapp.impl;

 

import java.util.List;

import cn.edu.bbc.computer.restfulapp.dao.StudentDao;

import cn.edu.bbc.computer.restfulapp.pojo.Student;

import cn.edu.bbc.computer.restfulapp.service.StudentService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

 

@Service

public class StudentServiceImpl implements StudentService {

    @Autowired

    private StudentDao studentdao;

 

    @Override

    public boolean addStudent(Student student) {

        boolean result = false;

        try {

            studentdao.addStudent(student);

            result = true;

        } catch (Exception e) {

            e.printStackTrace();

        }

        return result;

    }

 

    @Override

    public boolean updateStudent(Student student) {

        boolean result = false;

        try {

            studentdao.updateStudent(student);

            result = true;

        } catch (Exception e) {

            e.printStackTrace();

        }

        return result;

    }

 

    @Override

    public boolean deleteStudent(int id) {

        boolean result = false;

        try {

            studentdao.deleteStudent(id);

            result = true;

        } catch (Exception e) {

            e.printStackTrace();

        }

        return result;

    }

 

    @Override

    public Student findStudentByName(String sname) {

        return studentdao.findStudentByName(sname);

    }

 

    @Override

    public List<Student> findAll() {

        return studentdao.findAll();

    }

}

16、编写Controller 控制层

控制层这块和springMVC很像,但是相比而言要简洁不少。

@RestControllerRestful风格注解,使用这个注解,默认类中的方法都会以json的格式返回。

代码如下:

package cn.edu.bbc.computer.restfulapp.controller;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import cn.edu.bbc.computer.restfulapp.pojo.Student;

import cn.edu.bbc.computer.restfulapp.service.StudentService;

import java.util.*;

 

@RestController

@RequestMapping(value = "/api")

public class StudentRestController {

    @Autowired

    private StudentService studentService;

 

    @RequestMapping(value = "/student",method = RequestMethod.POST)

    public boolean addStudent(Student student) {

        System.out.println("开始新增...");

        System.out.println(student);

        return studentService.addStudent(student);

    }

 

    @RequestMapping(value = "/student", method = RequestMethod.PUT)

    public boolean updateStudent(Student student) {

        System.out.println("开始更新...");

        return studentService.updateStudent(student);

    }

 

    @RequestMapping(value = "/student", method = RequestMethod.DELETE)

    public boolean delete(@RequestParam(value = "studentName", required = true) int studentId) {

        System.out.println("开始删除...");

        return studentService.deleteStudent(studentId);

    }

 

    @RequestMapping(value = "/student", method = RequestMethod.GET)

    public Student findByStudentName(@RequestParam(value = "studentName", required = true) String studentName) {

        System.out.println("开始查询...");

        return studentService.findStudentByName(studentName);

    }

 

    @RequestMapping(value = "/studentAll", method = RequestMethod.GET)

    public List<Student> findByStudentAge() {

        System.out.println("开始查询所有数据...");

        return studentService.findAll();

    }

}

  1. Application 主程序

SpringApplication 则是用于从main方法启动Spring应用的类。默认,它会执行以下步骤:

  1. 创建一个合适的ApplicationContext实例 (取决于classpath)。
  2. 注册一个CommandLinePropertySource,以便将命令行参数作为Spring properties
  3. 刷新application context,加载所有单例beans
  4. 激活所有CommandLineRunner beans

直接使用main启动该类,SpringBoot便自动化配置了。

@SpringBootApplication:开启组件扫描和自动配置。

代码如下:

package cn.edu.bbc.computer.restfulapp;

 

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

 

@SpringBootApplication

public class RestfulappApplication {

public static void main(String[] args) {

SpringApplication.run(RestfulappApplication.class, args);

}

}

  1. 接口测试

这个留作课后练习了,使用curl构造表Student的增删改查,当然可以使用PostmanJMeter等类似的接口测试工具,其中curl的使用在4.10有详细的介绍,JMeter我在上学期讲实验课的时候也给你们详细介绍过。


0 条 查看最新 评论

没有评论
暂时无法发表评论