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(); } } }
工程看起来像这样:
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 了。