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.png

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)

首先,添加一个嵌入式的Java Servlet Container,如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 00:00>

Last updated: <2018-03-14 Wed 16:23>

Creator: Emacs 25.3.1 (Org mode 9.1.4)