RESTful in Java

Table of Contents

1. REST 简介

REST stands for REpresentational State Transfer.

REST 是一种 Web 架构风格。这种架构有下面 6 个约束:
(1) 客户-服务器(Client-Server):通信只能由客户端单方面发起,表现为请求-响应的形式。
(2) 无状态(Stateless):通信的会话状态(Session State)应该全部由客户端负责维护。
(3) 缓存(Cache):响应内容可以在通信链的某处被缓存,以改善网络效率。
(4) 统一接口(Uniform Interface):通信链的组件之间通过统一的接口(如 HTTP 方法 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)相互通信,以提高交互的可见性。
(5) 分层系统(Layered System):通过限制组件的行为(即,每个组件只能“看到”与其交互的紧邻层),将架构分解为若干等级的层。
(6) 按需代码(Code-On-Demand,可选):支持通过下载并执行一些代码(例如 Java Applet、Flash 或 JavaScript),对客户端的功能进行扩展。

To the extent that systems conform to the constraints of REST they can be called RESTful.

参考:http://www.infoq.com/cn/articles/understanding-restful-style

REST 架构的好处:

  • Performance - component interactions can be the dominant factor in user-perceived performance and network efficiency.
  • Scalability to support large numbers of components and interactions among components.
  • Simplicity of interfaces.
  • Modifiability of components to meet changing needs (even while the application is running).
  • Visibility of communication between components by service agents.
  • Portability of components by moving program code with the data.
  • Reliability is the resistance to failure at the system level in the presence of failures within components, connectors, or data.

参考:https://en.wikipedia.org/wiki/Representational_state_transfer

2. Java 实例:RESTful Web Services

2.1. Eclipse 中演示 Jersey 实例

下面通过一个简单的例子来演示在 Eclipse 集成开发环境中用 Java 实现 RESTful Web Services。例子中 JAX-RS(Java API for RESTful Web Services)的实现采用的是 Jersey.

这个例子是一个简单的用户管理应用,为了简单起见直接使用文件保存用户数据,没有使用数据库。
说明:由于 JAX-RS 标准中并不包含 JSON 的相关接口,要使用 JSON,还需要其它库及其依赖,这个例子使用的是 XML 格式,而没有使用 JSON。

参考:http://www.tutorialspoint.com/restful/restful_quick_guide.htm

2.1.1. 环境准备

首先,要安装 JDK 1.7, Apache Tomcat(或其它应用服务器),步骤略。
然后,从 https://jersey.java.net/download.html 下载 Jersey。解压后,会得到下面目录结构:

jaxrs-ri/
├── api
│   └── javax.ws.rs-api-2.0.1.jar
├── ext
│   ├── aopalliance-repackaged-2.4.0-b34.jar
│   ├── asm-debug-all-5.0.4.jar
│   ├── hk2-api-2.4.0-b34.jar
│   ├── hk2-locator-2.4.0-b34.jar
│   ├── hk2-utils-2.4.0-b34.jar
│   ├── javassist-3.18.1-GA.jar
│   ├── javax.annotation-api-1.2.jar
│   ├── javax.inject-2.4.0-b34.jar
│   ├── javax.servlet-api-3.0.1.jar
│   ├── jaxb-api-2.2.7.jar
│   ├── jersey-guava-2.23.1.jar
│   ├── org.osgi.core-4.2.0.jar
│   ├── osgi-resource-locator-1.0.1.jar
│   ├── persistence-api-1.0.jar
│   └── validation-api-1.1.0.Final.jar
├── Jersey-LICENSE.txt
├── lib
│   ├── jersey-client.jar
│   ├── jersey-common.jar
│   ├── jersey-container-servlet-core.jar
│   ├── jersey-container-servlet.jar
│   ├── jersey-media-jaxb.jar
│   └── jersey-server.jar
└── third-party-license-readme.txt

其中,子目录 ext 是它的依赖库。

2.1.2. 创建工程

这里以 Eclipse Java EE IDE 为例。

首先,创建一个工程,在菜单[File]->[New]->[Project]中选择“Dynamic Web Project”,给新工程命名为 UserManagement。
然后,把前面下载的 Jersey 包中的所有 jar 文件(包括解压后的 api,ext,lib 这 3 个子目录中所有 jar)复制到新创建工程的目录 WEB-INF/lib 中。

在包 com.tutorialspoint 下创建 3 个 java 文件。
User.java

package com.tutorialspoint;

import java.io.Serializable;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "user")
public class User implements Serializable {

  private static final long serialVersionUID = 1L;
  private int id;
  private String name;
  private String profession;

  public User(){}

  public User(int id, String name, String profession){
    this.id = id;
    this.name = name;
    this.profession = profession;
  }

  public int getId() {
    return id;
  }

  @XmlElement
  public void setId(int id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  @XmlElement
  public void setName(String name) {
    this.name = name;
  }

  public String getProfession() {
    return profession;
  }

  @XmlElement
  public void setProfession(String profession) {
    this.profession = profession;
  }
}

UserDao.java

package com.tutorialspoint;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

public class UserDao {
  public List<User> getAllUsers(){
    List<User> userList = null;
    try {
        File file = new File("Users.dat");
        if (!file.exists()) {
          User user = new User(1, "Mahesh", "Teacher");
          userList = new ArrayList<User>();
          userList.add(user);
          saveUserList(userList);
        }
        else{
          FileInputStream fis = new FileInputStream(file);
          ObjectInputStream ois = new ObjectInputStream(fis);
          userList = (List<User>) ois.readObject();
          ois.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return userList;
  }

  public User getUser(int id){
    List<User> users = getAllUsers();

    for(User user: users){
      if(user.getId() == id){
        return user;
      }
    }
    return null;
  }

  public int addUser(User pUser){
    List<User> userList = getAllUsers();
    boolean userExists = false;
    for(User user: userList){
      if(user.getId() == pUser.getId()){
        userExists = true;
        break;
      }
    }
    if(!userExists){
      userList.add(pUser);
      saveUserList(userList);
      return 1;
    }
    return 0;
  }

  private void saveUserList(List<User> userList){
    try {
        File file = new File("Users.dat");
        FileOutputStream fos;

        fos = new FileOutputStream(file);

        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(userList);
        oos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
  }
}

UserService.java

package com.tutorialspoint;

import java.io.IOException;
import java.util.List;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/UserService")
public class UserService {

  UserDao userDao = new UserDao();

  @GET
  @Path("/users")
  @Produces(MediaType.APPLICATION_XML)
  public List<User> getUsers(){
    return userDao.getAllUsers();
  }

  @GET
  @Path("/users/{userid}")
  @Produces(MediaType.APPLICATION_XML)
  public User getUser(@PathParam("userid") int userid){
    return userDao.getUser(userid);
  }

  @PUT
  @Path("/users")
  @Produces(MediaType.APPLICATION_XML)
  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
  public Response createUser(@FormParam("id") int id,
                           @FormParam("name") String name,
                           @FormParam("profession") String profession,
                           @Context HttpServletResponse servletResponse) throws IOException{
    User user = new User(id, name, profession);
    int result = userDao.addUser(user);
    if(result == 1){
        return Response.status(201).entity(user.toString()).build();
    } else {
        return Response.status(400).entity("FAIL.").build();
    }
  }
}

工程看起来像这样:

restful.gif

Figure 1: 第一个 RESTful 应用

2.1.2.1. 创建 web.xml,导出 war 包

创建配置文件 web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://java.sun.com/xml/ns/javaee"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
   http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
   id="WebApp_ID" version="3.0">
   <display-name>User Management</display-name>

   <servlet>
      <servlet-name>Jersey RESTful Application</servlet-name>
      <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
         <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.tutorialspoint</param-value>
         </init-param>
      </servlet>
   <servlet-mapping>
   <servlet-name>Jersey RESTful Application</servlet-name>
      <url-pattern>/rest/*</url-pattern>
   </servlet-mapping>
</web-app>

有 Eclipse 菜单中,选择[File]->[export]->[Web]->[War File],可导出 war 文件 UserManagement.war。

2.1.3. 部署和测试

复制 UserManagement.war 到 tomcat 的 webapps 目录中,启动 tomcat 后,会自动解压 war 包到同一目录。

$ ls webapps/
docs  examples  host-manager  manager  ROOT  UserManagement  UserManagement.war

在 tomcat 安装目录,用 ./bin/startup.sh 启动 tomcat。

测试 GET(也可以直接在浏览器中打开相应网址):

$ curl -i http://localhost:8080/UserManagement/rest/UserService/users
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/xml
Content-Length: 144
Date: Tue, 14 Jun 2016 11:33:47 GMT

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><users><user><id>1</id><name>Mahesh</name><profession>Teacher</profession></user></users>

$ curl -i http://localhost:8080/UserManagement/rest/UserService/users/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/xml
Content-Length: 129
Date: Tue, 14 Jun 2016 12:39:49 GMT

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><user><id>1</id><name>Mahesh</name><profession>Teacher</profession></user>

PUT 的测试相对麻烦些,可参考:http://www.tutorialspoint.com/restful/restful_quick_guide.htm

2.2. Maven 中演示 Jersey 实例

和手动管理依赖相比,用 Maven 自动管理依赖更加方便。

下面将演示在 Maven 中使用 Jersey 的例子。这个例子比前面的更加简单,使用的是 jersey 自带的 quickstart 例子。

2.2.1. 用 Maven 创建工程

用 Maven 创建工程。这里创建 Jersey 自带的例子。如:

$ mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-webapp \
                -DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \
                -DgroupId=com.example -DartifactId=simple-service-webapp -Dpackage=com.example \
                -DarchetypeVersion=2.25

成功执行后,生成的目录结构如下:

$ find .
.
./simple-service-webapp
./simple-service-webapp/pom.xml
./simple-service-webapp/src
./simple-service-webapp/src/main
./simple-service-webapp/src/main/java
./simple-service-webapp/src/main/java/com
./simple-service-webapp/src/main/java/com/example
./simple-service-webapp/src/main/java/com/example/MyResource.java
./simple-service-webapp/src/main/resources
./simple-service-webapp/src/main/webapp
./simple-service-webapp/src/main/webapp/index.jsp
./simple-service-webapp/src/main/webapp/WEB-INF
./simple-service-webapp/src/main/webapp/WEB-INF/web.xml

生成的文件“pom.xml”,其内容如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>simple-service-webapp</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>simple-service-webapp</name>

    <build>
        <finalName>simple-service-webapp</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <inherited>true</inherited>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <!-- use the following artifactId if you don't need servlet 2.x compatibility -->
            <!-- artifactId>jersey-container-servlet</artifactId -->
        </dependency>
        <!-- uncomment this to get JSON support
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-moxy</artifactId>
        </dependency>
        -->
    </dependencies>
    <properties>
        <jersey.version>2.25</jersey.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

生成的文件“MyResource.java”,其内容如下:

package com.example;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

/**
 * Root resource (exposed at "myresource" path)
 */
@Path("myresource")
public class MyResource {

    /**
     * Method handling HTTP GET requests. The returned object will be sent
     * to the client as "text/plain" media type.
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getIt() {
        return "Got it!";
    }
}

生成的文件“web.xml”,其内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
     see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.example</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/webapi/*</url-pattern>
    </servlet-mapping>
</web-app>

2.2.2. 部署测试(使用 jetty)

我们可以在第一步完成后,进入到工程目录,用 mvn package 生成 war 包,再部署到 Tomcat 中可进行测试。但这样很麻烦,我们将使用 Jetty 作为嵌入式的 Java Servlet Container,这样可以简化部署过程。

只需把 Jetty 的相关信息加入到 pom.xml 中即可。

......
    <build>
        <finalName>simple-service-webapp</finalName>
        <plugins>
            <plugin>
               <groupId>org.eclipse.jetty</groupId>
               <artifactId>jetty-maven-plugin</artifactId>
               <version>9.2.17.v20160517</version>
            </plugin>
......

最后,进入工程目录,执行 mvn jetty:run 即可一步完成编译部署。如:

$ cd simple-service-webapp
$ mvn jetty:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building simple-service-webapp 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
......
[INFO] Started ServerConnector@5a50d9fc{HTTP/1.1}{0.0.0.0:8080}
[INFO] Started @4213ms
[INFO] Started Jetty Server

用浏览器(或 curl,wget 等工具)打开 http://127.0.0.1:8080/webapi/myresource 即可测试 REST Service 是否工作:

$ curl -i http://127.0.0.1:8080/webapi/myresource
HTTP/1.1 200 OK
Date: Wed, 07 Mar 2018 09:36:43 GMT
Content-Type: text/plain
Content-Length: 7
Server: Jetty(9.2.17.v20160517)

Got it!
2.2.2.1. 启用热部署(有 class 文件修改就自动重启)

修改 Java 文件后,如何能自动应用新改动,则大大方便开发调试过程。这可以通过下面两个设置来实现:
1、设置自动编译 Java 为 class 文件。如果使用 Eclispe,点击“Project”->“Build Automatically”即可。
2、在 pom.xml 中配置 jetty 的参数 scanIntervalSeconds 。比如,配置每 2 秒钟扫描 class 文件:

......
    <build>
        <finalName>simple-service-webapp</finalName>
        <plugins>
            <plugin>
               <groupId>org.eclipse.jetty</groupId>
               <artifactId>jetty-maven-plugin</artifactId>
               <version>9.2.17.v20160517</version>
               <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
               </configuration>
            </plugin>
......

2.2.3. 返回 JSON 数据

前面介绍的 Jersey 自带例子,返回的数据是“text/plain”类型。下面介绍返回 JSON 数据的实例。

首先,打开 pom.xml,增加 JSON 的支持库 jackson(这个例子中不使用 MOXy):

        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
        </dependency>

然后,把文件“MyResource.java”的内容修改为:

package com.example;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

/**
 * Root resource (exposed at "myresource" path)
 */
@Path("myresource")
public class MyResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public MyJsonTest getIt() {
        return new MyJsonTest("Got it!");
    }
}

class MyJsonTest {
    private String str;

    public MyJsonTest(String input) {
        str = input;
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }
}

重新部署测试如下:

$ curl -i http://127.0.0.1:8080/webapi/myresource
HTTP/1.1 200 OK
Date: Wed, 07 Mar 2018 09:54:31 GMT
Content-Type: application/json
Content-Length: 17
Server: Jetty(9.2.17.v20160517)

{"str":"Got it!"}

可发现,返回的数据已经是 JSON 了。

Author: cig01

Created: <2016-06-14 Tue>

Last updated: <2019-01-03 Thu>

Creator: Emacs 27.1 (Org mode 9.4)