1.5 快速实现Java REST服务

本节包含两个Jersey实战示例,目的是让读者具备快速创建REST服务的能力。

在开始实战之前,首先需要读者确认你的环境是否已经安装了Java和Maven。这里使用Maven命令,示例如下。

    mvn -v
    Apache  Maven  3.3.3  (7994120775791599e205a5524ec3e0dfe41d4a06;  2015-04-
    22T19:57:37+08:00)
    Maven home: /usr/local/Cellar/maven/3.3.3/libexec
    Java version: 1.8.0_40, vendor: Oracle Corporation
    Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre
    Default locale: zh_CN, platform encoding: UTF-8
    OS name: "mac os x", version: "10.11.1", arch: "x86_64", family: "mac"

从Maven版本显示命令的结果中,自上而下可以看到Maven的版本信息和HOME路径信息、Java的版本信息和HOME路径信息、本地语言、平台字符集以及操作系统信息。

1.5.1 第一个REST服务

Jersey提供了Maven原型(archetype)来快速创建REST服务项目。

1.创建项目

我们首先使用archetypeGroupId为org.glassfish.jersey.archetypes的原型、archetypeArtifactId为jersey-quickstart-grizzly2的原型,创建REST服务项目。示例如下。

    mvn archetype:generate \
    -DarchetypeArtifactId=jersey-quickstart-grizzly2 \
    -DarchetypeGroupId=org.glassfish.jersey.archetypes \
    -DinteractiveMode=false \
    -DgroupId=my.restful \
    -DartifactId=my-first-service \
    -Dpackage=my.restful \
    -DarchetypeVersion=2.22.1

上述命令将创建一个标准的Maven工程。其中,interactiveMode=false代表无需交互,archetypeVersion指定原型的版本,这个版本与Jersey的版本一致。groupId、artifactId和package分别定义了我们这个项目的组ID为my.restful,工件ID为my-first-service,包名为my.restful。我们通过观察项目根目录下的pom.xml,可以对应出上述命令参数与Maven坐标的关系。相关部分的示例如下。

    <groupId>my.restful</groupId>
    <artifactId>my-first-service</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>my-first-service</name>

2.运行服务

Maven工程建立好后,我们首先启动REST服务体验一下该项目的功能。进入项目的根目录,并执行如下命令构建和启动服务。

    cd my-first-service
    mvn package
    mvn exec:java
    …
    Jersey  app  started  with  WADL  available  at  http://localhost:8080/myapp/
    application.wadl
    Hit enter to stop it...

该命令启动了REST服务,端口是8080,我们可以随时通过回车键停止这个服务。同时,该服务还提供了WADL(详见1.6节)。通过访问application.wadl,可以获取当前REST服务公布的接口。本例WADL的关键部分,示例如下。

    <ns0:resources base="http://localhost:8080/myapp/">
        <ns0:resource path="myresource">
            <ns0:method id="getIt" name="GET">
                <ns0:response>
                      <ns0:representation mediaType="text/plain" />
                </ns0:response>
            </ns0:method>
        </ns0:resource>
    </ns0:resources>

这里定义了一个资源路径myresource,在该路径下,定义了一个GET方法getIt,表述类型为text/plain。

3.访问服务

我们使用cURL(详见1.8节)来访问REST服务公布的myresource资源方法getIt,示例如下。

    curl http://localhost:8080/myapp/myresource
    Got it!

HTTPie(读作H-T-T-Pie)是和cURL类似的CLI工具,但交互上更人性化。我们使用HTTPie请求相同的资源地址,请求和响应信息如下。

    http http://localhost:8080/myapp/myresource
    HTTP/1.1200 OK
    Content-Length: 7
    Content-Type: text/plain
    Date: Sat, 14 Nov 2015 04:08:54 GMT
    Got it!

响应信息的第一行包含了HTTP协议版本和状态码,接下来是部分HTTP HEAD信息,最后是HTTP BODY信息。cURL携带-i或者--include参数可以得到相同的结果,示例如下。

    curl -i http://localhost:8080/myapp/myresource
    HTTP/1.1200 OK
    Content-Type: text/plain
    Date: Sat, 14 Nov 2015 04:08:54 GMT
    Content-Length: 7
    Got it!

要想获得更多的cURL请求响应信息,可以使用-v参数,示例如下。

    curl -v http://localhost:8080/myapp/myresource
    * Hostname was NOT found in DNS cache
    *    Trying ::1...
    * connect to ::1 port 8080 failed: Connection refused
    *    Trying fe80::1...
    * connect to fe80::1 port 8080 failed: Connection refused
    *    Trying 127.0.0.1...
    * Connected to localhost (127.0.0.1) port 8080 (#0)
    > GET /myapp/myresource HTTP/1.1
    > User-Agent: curl/7.38.0
    > Host: localhost:8080
    > Accept: */*
    >
    < HTTP/1.1200 OK
    < Content-Type: text/plain
    < Date: Sat, 14 Nov 2015 04:08:56 GMT
    < Content-Length: 7
    <
    * Connection #0 to host localhost left intact
    Got it!

4.分析项目

完成了最初的体验后,我们来分析下面这个示例工程。首先,从启动服务的命令mvn exec:java入手。该命令实际调用了exec-maven-plugin插件中定义的一个值为java的goal,用以触发mainClass中的main函数。本例的mainClass定义为my.restful.Main。在pom.xml中,exec插件完整定义如下。

    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.2.1</version>
        <executions>
            <execution>
                <goals>
                      <goal>java</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <mainClass>my.restful.Main</mainClass>
        </configuration>
    </plugin>

除了pom.xml和Main类,示例还包含哪些内容呢?我们可以使用如下命令查看。

源代码中,还包括了资源类MyResource和它的单元测试类MyResourceTest。

在资源类MyResource中,@Path中定义了资源路径,@GET中定义了GET方法getIt(), @Produces中定义了响应的类型为普通的字符串,示例如下。

    @Path("myresource")
    public class MyResource {
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public String getIt() {
            return "Got it! ";
        }
    }

相应地,资源测试类MyResourceTest中实现了对getIt()方法的测试,示例如下。

    @Test
    public void testGetIt() {
        String responseMsg = target.path("myresource").request().get(String.class);
        assertEquals("Got it! ", responseMsg);
    }

在上述代码中,target()、path()、request()和get()方法都是Jersey Client中定义的方法,这些方法组合在一起,形成了流式风格的API。我们期待响应值为“Got it! ”的字符串。

5.单元测试

最后,我们使用如下命令执行单元测试。使用IDE可以直接通过图形界面单击对该方法的测试。

    mvn test
    …
    -------------------------------------------------------
      T E S T S
    -------------------------------------------------------
    Running my.restful.MyResourceTest
    …
    Results:
    Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

jersey-quickstart-grizzly2原型提供的模板代码,使用了main函数,并在其中启动了Grizzly的HttpServer。这是典型的Java SE形式的REST应用。更多情况下,我们希望得到的是一个可以以war包形式部署到Servlet容器的轻量级Java EE项目。接下来的示例就是这样的Web形式的项目。

1.5.2 第一个Servlet容器服务

jersey-quickstart-webapp原型会为我们生成Servlet容器服务。

1.创建项目

使用如下命令创建名为my-first-webapp的Web项目。

    mvn archetype:generate \
    -DarchetypeArtifactId=jersey-quickstart-webapp \
    -DarchetypeGroupId=org.glassfish.jersey.archetypes \
    -DinteractiveMode=false \
    -DgroupId=my.restful \
    -DartifactId=my-first-webapp \
    -Dpackage=my.restful \
    -DarchetypeVersion=2.22.1

2.运行服务

由于这是一个Web项目,没有main函数,我们必须将其部署到Servlet容器(比如Tomcat、Jetty)中,才能将其运行。在开发阶段,我们无需真正将其部署,而是使用Maven插件这种更轻量级的方式启动服务。在pom.xml中,增加如下定义来添加插件。

    <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>9.3.5.v20151012</version>
    </plugin>

有了插件,我们可以使用如下命令编译和启动服务,使用Ctrl+C停止服务。

    mvn jetty:run

如果我们要对示例项目进行断点调试,应在服务启动前设置监听端口等信息。这里以IntelliJ IDEA所使用的5050端口为例,示例如下。

    export  MAVEN_OPTS="-Xdebug  -Xnoagent  -Djava.compiler=NONE  -Xrunjdwp:
    transport=dt_socket, address=5050, server=y, suspend=y"
    mvn jetty:run

以这样的方式启动服务,需要IDE与之交互。过程是首先启动端口,然后IDE向该端口请求监听,服务启动并接收请求,在代码的某个断点处,服务会向该端口推送事件,IDE在代码的断点处停留并高亮显示该行。

3.访问服务

服务启动后,我们使用HTTPie请求资源地址,示例如下。

    http http://localhost:8080/webapi/myresource
    HTTP/1.1200 OK
    Content-Length: 7
    Content-Type: text/plain
    Date: Sat, 14 Nov 2015 08:00:03 GMT
    Server: Jetty(9.3.5.v20151012)
    Got it!

4.分析项目

本例是一个标准的Maven Web工程。Web的根目录默认名称为webapp,默认的Servlet版本为2.5,需要使用WEB-INF/web.xml文件来配置REST服务。我们通过tree命令得到完整的工程结构如下。

5.扩展项目

本例与前例提供的资源类和资源方法相同,我们在此基础上增加两个资源方法,分别用来新增和查询资源,示例如下。

    private static ConcurrentHashMap<String, MyDomain> map=new ConcurrentHashMap<>();
    @GET
    @Path("{key}")
    @Produces(MediaType.APPLICATION_XML)
    public MyDomain getMy(@PathParam("key") final String key) {
        final MyDomain myDomain = map.get(key);
        if (myDomain == null) {
            return new MyDomain();
        }
        return myDomain;
    }
    @POST
    @Consumes(MediaType.APPLICATION_XML)
    public void addMy(final MyDomain myDomain) {
        map.put(myDomain.getName(), myDomain);
    }

如上所示,POST方法addMy用于接收并存储新增的表述,GET方法getMy用于查询表述。MyDomain类是基于JAXB的POJO类,用于表示XML格式的表述。

首先,我们通过如下命令,新增一条记录。

    curl -X POST http://localhost:8080/webapi/myresource -d '<myDomain name="eric"
    value="feuyeux@gmail.com"/>' -H "Content-type:application/xml"

然后通过如下命令查询和验证新增记录的存在。

    curl http://localhost:8080/webapi/myresource/eric
    <? xml  version="1.0"  encoding="UTF-8"  standalone="yes"? ><myDomain  name="eric"
    value="feuyeux@gmail.com"/>