# 用 Jersey 2 和 Spring 4 構建 RESTful web service
本文介紹了如何通過 Jersey 框架優美的在 Java 實現了 REST 的 API。CRUD 的 操作存儲在 MySQL 中
## 1\. 示例
### 1.1 為什么
Spring 可以對于 REST 有自己的實現(見 [https://spring.io/guides/tutorials/rest/](https://spring.io/guides/tutorials/rest/))。 但本文展示的是用 “官方” 的 方法來實現 REST ,即使用 Jersey。
### 1.2 它是做什么的?
管理 資源。 REST API 將允許創建、檢索、更新和刪除這樣的資源。
### 1.3 架構及技術
本示例項目使用多層結構,基于“Law of Demeter (LoD) or principle of least knowledge”(迪米特法則),是說一個軟件實體要盡可能的只與和它最近的實體進行通訊。通常被表述為:talk only to your immediate friends ( 只和離你最近的朋友進行交互)。
“talk”,其實就是對象間方法的調用。這條規則表明了對象間方法調用的原則:(1)調用對象本身的方法;(2)調用通過參數傳入的對象的方法;(3)在方法中創建的對象的方法;(4)所包含對象的方法。
主要分為三層:
* 第一層:Jersey 實現對 REST 的支持,擁有 [外觀模式](http://en.wikipedia.org/wiki/Facade_pattern)的角色并代理到邏輯業務層
* 業務層: 發生邏輯的地方
* 數據訪問層: 是與持久數據存儲(在我們的例子中是 MySql數據庫)交互的地方
簡述下技術框架:
#### 1.3.1\. Jersey (外觀)
[Jersey](https://jersey.java.net/) 是開源、擁有產品級別的質量,提供構建 RESTful Web Services,支持 JAX-RS APIs ,提供 [JAX-RS](https://jax-rs-spec.java.net/) (JSR 311 & JSR 339) 參考實現。
#### 1.3.2\. Spring (業務層)
在我看來沒有什么 比 [Spring](http://projects.spring.io/spring-framework/) 更好的辦法讓 pojo 具有不同的功能。 你會發現在本教程用 Jersey 2 和 Spring 4 構建 RESTful web service
#### 1.3.3\. JPA 2 / Hibernate (持久層)
使用 Hibernate 實現 DAO 模式。
#### 1.3.4\. Web 容器
用 Maven 打包成 .war 文件開源部署在任意容器。一般用 [Tomcat](http://tomcat.apache.org/) 和 [Jetty](http://www.eclipse.org/jetty/) ,也可以是 Glassfih, Weblogic, JBoss 或 WebSphere.
#### 1.3.5\. MySQL 數據庫
示例數據存儲在一個 MySQL 表:
#### 1.3.6\. 技術版本
Jersey 2.9
Spring 4.0.3
Hibernate 4
Maven 3
Tomcat 7
Jetty 9
MySql 5.6
### 1.4\. 源碼
見[https://github.com/waylau/RestDemo/tree/master/jersey-2-spring-4-rest](https://github.com/waylau/RestDemo/tree/master/jersey-2-spring-4-rest)
## 2\. 配置
開始呈現 REST API 的設計和實現之前,我們需要做一些配置。
### 2.1\. 項目依賴
[Jersey Spring 擴展包](https://github.com/waylau/Jersey-2.x-User-Guide/tree/master/Chapter%2022.%20Spring%20DI%20%E4%BD%BF%E7%94%A8%20Spring%20%E6%B3%A8%E5%85%A5) 是必須要放在 項目 classpath 中。在 pom.xml 中添加下面依賴:
```
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring3</artifactId>
<version>${jersey.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.4.1</version>
</dependency>
```
_注意: jersey-spring3.jar 使用的是他自己的 Spring 庫版本,所以如果你想使用自己的 (本例是使用 Spring 4.0.3.Release),你需要將這些庫手動的移除。如果想看到其他 的庫的依賴,請查看項目源碼中的 pom.xml_
### 2.2\. web.xml
應用部署描述
```
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 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_3_0.xsd">
<display-name>Demo - Restful Web Application</display-name>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>jersey-serlvet</servlet-name>
<servlet-class>
org.glassfish.jersey.servlet.ServletContainer
</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>org.codingpedia.demo.rest.RestDemoJaxRsApplication</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jersey-serlvet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<resource-ref>
<description>Database resource rest demo web application </description>
<res-ref-name>jdbc/restDemoDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</web-app>
```
#### 2.2.1\. Jersey-servlet
注意 Jersey servlet 的配置,`javax.ws.rs.core.Application` 類定義了 JAX-RS 應用組件(root 資源 和 提供者 類) .本例使用 `ResourceConfig`, 是 Jersey 自己實現的 `Application` 類,提供了簡化 JAX-RS 組件的能力。詳見[JAX-RS 應用模型](https://github.com/waylau/Jersey-2.x-User-Guide/blob/master/Chapter%204.%20Application%20Deployment%20and%20Runtime%20Environments%20%E5%BA%94%E7%94%A8%E9%83%A8%E7%BD%B2%E5%92%8C%E8%BF%90%E8%A1%8C%E6%97%B6%E7%8E%AF%E5%A2%83/4.2.%20JAX-RS%20Application%20Model%20%E5%BA%94%E7%94%A8%E6%A8%A1%E5%9E%8B.md)
`org.codingpedia.demo.rest.RestDemoJaxRsApplication` 是自己實現的 `ResourceConfig`類,注冊應用的 resources, filters, exception mappers 和 feature :
```
package org.codingpedia.demo.rest.service;
//imports omitted for brevity
/**
* Registers the components to be used by the JAX-RS application
*
* @author ama
*
*/
public class RestDemoJaxRsApplication extends ResourceConfig {
/**
* Register JAX-RS application components.
*/
public RestDemoJaxRsApplication() {
// register application resources
register(PodcastResource.class);
register(PodcastLegacyResource.class);
// register filters
register(RequestContextFilter.class);
register(LoggingResponseFilter.class);
register(CORSResponseFilter.class);
// register exception mappers
register(GenericExceptionMapper.class);
register(AppExceptionMapper.class);
register(NotFoundExceptionMapper.class);
// register features
register(JacksonFeature.class);
register(MultiPartFeature.class);
}
}
```
注意:
* `org.glassfish.jersey.server.spring.scope.RequestContextFilter` 是 Spring filter 提供了 JAX-RS 和 Spring 請求屬性之間的橋梁。
* `org.codingpedia.demo.rest.resource.PodcastsResource` 這是“外觀”組件,通過注解 暴露了 REST 的API。稍后會描述
* `org.glassfish.jersey.jackson.JacksonFeature`,是一個 feature ,用 Jackson JSON 的提供者來解釋 JSON。
#### 2.1.2\. Spring 配置
配置文件在 classpath 目錄下的 spring/applicationContext.xml:
```
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.codingpedia.demo.rest.*" />
<!-- ************ JPA configuration *********** -->
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<bean id="transactionManagerLegacy" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactoryLegacy" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" />
<property name="persistenceUnitName" value="demoRestPersistence" />
<property name="dataSource" ref="restDemoDS" />
<property name="packagesToScan" value="org.codingpedia.demo.*" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true" />
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
</bean>
</property>
</bean>
<bean id="entityManagerFactoryLegacy" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" />
<property name="persistenceUnitName" value="demoRestPersistenceLegacy" />
<property name="dataSource" ref="restDemoLegacyDS" />
<property name="packagesToScan" value="org.codingpedia.demo.*" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true" />
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
</bean>
</property>
</bean>
<bean id="podcastDao" class="org.codingpedia.demo.rest.dao.PodcastDaoJPA2Impl"/>
<bean id="podcastService" class="org.codingpedia.demo.rest.service.PodcastServiceDbAccessImpl" />
<bean id="podcastsResource" class="org.codingpedia.demo.rest.resource.PodcastsResource" />
<bean id="podcastLegacyResource" class="org.codingpedia.demo.rest.resource.PodcastLegacyResource" />
<bean id="restDemoDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton">
<property name="jndiName" value="java:comp/env/jdbc/restDemoDB" />
<property name="resourceRef" value="true" />
</bean>
<bean id="restDemoLegacyDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton">
<property name="jndiName" value="java:comp/env/jdbc/restDemoLegacyDB" />
<property name="resourceRef" value="true" />
</bean>
</beans>
```
其中 `podcastsResource`是指向 REST API 實體
## 3\. REST API (設計與實現)
### 3.1\. 資源
#### 3.1.1\. 設計
REST 中的資源主要包括下面兩大思想:
* 每個都指向了全球標示符(如,HTTP 中的 [URI](http://en.wikipedia.org/wiki/Uniform_resource_identifier))
* 有一個或多個表示(我們將在本示例使用 JSON 格式)
REST 中的資源 一般是名詞 (podcasts, customers, user, accounts 等) 而不是名詞 (getPodcast, deleteUser 等)
本教程使用的端點有:
* `/podcasts` – (注意復數)URI標識的資源 podcasts 集合的播客
* `/podcasts/{id}` – 通過 podcasts 的ID, URI 標識一個podcasts 資源,
#### 3.1.2\. 實現
為求精簡 , podcast 只包含下列屬性:
* id – podcast 的唯一標識
* feed – podcast 的 feed url
* title – 標題
* linkOnPodcastpedia – 鏈接
* description – 描述
我用了兩種 Java 類來表示 podcast 代碼,是為了避免 類及其屬性/方法 被 JPA 和 XML/JAXB/JSON 的注釋堆滿了:
* PodcastEntity.java – JPA 注解類用在 DB 和業務層
* Podcast.java – JAXB/JSON 注解類用在外觀和業務層
Podcast.java
```
package org.codingpedia.demo.rest.resource;
//imports omitted for brevity
/**
* Podcast resource placeholder for json/xml representation
*
* @author ama
*
*/
@SuppressWarnings("restriction")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Podcast implements Serializable {
private static final long serialVersionUID = -8039686696076337053L;
/** id of the podcast */
@XmlElement(name = "id")
private Long id;
/** title of the podcast */
@XmlElement(name = "title")
private String title;
/** link of the podcast on Podcastpedia.org */
@XmlElement(name = "linkOnPodcastpedia")
private String linkOnPodcastpedia;
/** url of the feed */
@XmlElement(name = "feed")
private String feed;
/** description of the podcast */
@XmlElement(name = "description")
private String description;
/** insertion date in the database */
@XmlElement(name = "insertionDate")
@XmlJavaTypeAdapter(DateISO8601Adapter.class)
@PodcastDetailedView
private Date insertionDate;
public Podcast(PodcastEntity podcastEntity){
try {
BeanUtils.copyProperties(this, podcastEntity);
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public Podcast(String title, String linkOnPodcastpedia, String feed,
String description) {
this.title = title;
this.linkOnPodcastpedia = linkOnPodcastpedia;
this.feed = feed;
this.description = description;
}
public Podcast(){}
//getters and setters now shown for brevity
}
```
轉化成 JSON 輸出如下
```
{
"id":1,
"title":"Quarks & Co - zum Mitnehmen-modified",
"linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/1/Quarks-Co-zum-Mitnehmen",
"feed":"http://podcast.wdr.de/quarks.xml",
"description":"Quarks & Co: Das Wissenschaftsmagazin",
"insertionDate":"2014-05-30T10:26:12.00+0200"
}
```
### 3.2\. 方法
簡單的說明
* 創建 = POST
* 讀 = GET
* 更新 = PUT
* 刪除 = DELETE
但不是一一映射,因為 PUT 也可以創建, POST也可以用在更新。
_注:讀取和刪除它是很清楚的,他們確實是和 GET 、DELETE一對一的映射。無論如何,REST 是一種架構風格,不是一個規范,你應該適應你的架構需要,但如果你想讓你的 API 讓更多的公眾愿意使用它,你應該遵循一定的“最佳實踐”。_
`PodcastRestResource` 類 是處理所有的請求
```
package org.codingpedia.demo.rest.resource;
//imports
......................
@Component
@Path("/podcasts")
public class PodcastResource {
@Autowired
private PodcastService podcastService;
.....................
}
```
注意 類定義前面的 `@Path("/podcasts")`,所有與 podcast 關聯的資源都會出現在 這個路徑下。 `@Path` 注解值是關聯 URI 的路徑。
在上面的例子中,該 Java 類將托管在`/podcasts` URI 路徑。`PodcastService` 接口公開的業務邏輯 到 REST 外觀層。
#### 3.2.1\. 創建 podcast
#### 3.2.1.1\. 設計
常見的的方式利用 POST 創建資源,如前所述,創建一個新的資源,可以用 POST 和 PUT 的方法,我是這樣做的:
| **? Description** | **? URI** | **? HTTP method
** | **? HTTP Status response** |
| --- | --- | --- | --- |
| ?增加新的 podcast | ?/podcasts/ | POST | 201 Created |
| ?增加新的 podcast (必須傳所有的值) | ?/podcasts/{id} | PUT | 201 Created |
PUT POST 最大的區別是 ,PUT 就是把你應該事先知道資源將被創建的位置和發送所有可能值的實體。
##### 3.2.1.2\. 實現
###### 3.2.1.2.1\. POST 創建一個單資源
```
/**
* Adds a new resource (podcast) from the given json format (at least title
* and feed elements are required at the DB level)
*
* @param podcast
* @return
* @throws AppException
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response createPodcast(Podcast podcast) throws AppException {
Long createPodcastId = podcastService.createPodcast(podcast);
return Response.status(Response.Status.CREATED)// 201
.entity("A new podcast has been created")
.header("Location",
"http://localhost:8888/demo-rest-jersey-spring/podcasts/"
+ String.valueOf(createPodcastId)).build();
}
```
注解
* `@POST` – 指示方法響應到 HTTP POST 請求
* @Consumes({MediaType.APPLICATION_JSON}) – 定義方法可以接受的媒體類型,本例為"application/json"
* @Produces({MediaType.TEXT_HTML}) – 定義方法產生的媒體類型本例為 "text/html"
響應
* 成功: HTTP 狀態 為 201 的 text/html 文件和頭的位置指定的資源已被創建
* 錯誤:
* 400:沒有足夠的數據提供
* 409:沖突了。如果在服務器端被確定 具有相同的 podcast 的存在
###### 3.2.1.2.2\. 通過 PUT 創建單資源 (“podcast”)
這將執行 更新 Podcast 處理。
###### 3.2.1.2.3\. 附加 – 通過表單創建 (“podcast”)資源
```
/**
* Adds a new podcast (resource) from "form" (at least title and feed
* elements are required at the DB level)
*
* @param title
* @param linkOnPodcastpedia
* @param feed
* @param description
* @return
* @throws AppException
*/
@POST
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
@Produces({ MediaType.TEXT_HTML })
@Transactional
public Response createPodcastFromApplicationFormURLencoded(
@FormParam("title") String title,
@FormParam("linkOnPodcastpedia") String linkOnPodcastpedia,
@FormParam("feed") String feed,
@FormParam("description") String description) throws AppException {
Podcast podcast = new Podcast(title, linkOnPodcastpedia, feed,
description);
Long createPodcastid = podcastService.createPodcast(podcast);
return Response
.status(Response.Status.CREATED)// 201
.entity("A new podcast/resource has been created at /demo-rest-jersey-spring/podcasts/"
+ createPodcastid)
.header("Location",
"http://localhost:8888/demo-rest-jersey-spring/podcasts/"
+ String.valueOf(createPodcastid)).build();
}
```
注解
* `@POST` – 指示方法響應到 HTTP POST 請求
* `@Consumes({MediaType.APPLICATION_FORM_URLENCODED})` – 定義方法可以接受的媒體類型,本例為"application/x-www-form-urlencoded"
* `@FormParam` – 這個注解綁定的表單參數值包含了請求對應資源方法參數的實體。值是 URL 的解碼,除非 禁用 解碼的注解。
* `@Produces({MediaType.TEXT_HTML})` – 定義方法產生的媒體類型本例為 "text/html"
響應
* 成功: HTTP 狀態 為 201 的 text/html 文件和頭的位置指定的資源已被創建
* 錯誤:
* 400:沒有足夠的數據提供
* 409:沖突了。如果在服務器端被確定 具有相同的 podcast 的存在
#### 3.2.2\. 讀 podcast
##### 3.2.2.1\. 設計
API 支持兩種操作
* 返回 podcast 的集合
* 根據 id 返回 podcast
注意到集合資源的參數–rderByInsertionDate 和 numberDaysToLookBack。在URI查詢參數添加過濾器而不是路徑的一部分這個是很有道理的。
##### 3.2.2.2\. 實現
###### 3.2.2.2.1\. 獲取所有 podcasts (“/”)
```
/**
* Returns all resources (podcasts) from the database
*
* @return
* @throws IOException
* @throws JsonMappingException
* @throws JsonGenerationException
* @throws AppException
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public List<Podcast> getPodcasts(
@QueryParam("orderByInsertionDate") String orderByInsertionDate,
@QueryParam("numberDaysToLookBack") Integer numberDaysToLookBack)
throws JsonGenerationException, JsonMappingException, IOException,
AppException {
List<Podcast> podcasts = podcastService.getPodcasts(
orderByInsertionDate, numberDaysToLookBack);
return podcasts;
}
```
注解
* `@GET` – 指示方法響應到 HTTP GET 請求
* `@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})` – 定義方法可以接受的媒體類型,本例為"application/json" 或者 "application/xml"(在 Podcast 類前 添加 `@XmlRootElement` ),將返回 JSON 或者 XML 格式的 podcast 集合
響應
* 成功: HTTP 狀態 為 200 的 podcast 數據集合
###### 3.2.2.2.1\. 讀一個 podcast
根據 id 獲取一個 podcast
```
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getPodcastById(@PathParam("id") Long id)
throws JsonGenerationException, JsonMappingException, IOException,
AppException {
Podcast podcastById = podcastService.getPodcastById(id);
return Response.status(200).entity(podcastById)
.header("Access-Control-Allow-Headers", "X-extra-header")
.allow("OPTIONS").build();
}
```
注解
* `@GET` – 指示方法響應到 HTTP GET 請求
* ` @PathParam("id")`- 綁定傳遞的參數值
* `@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})` – 定義方法可以接受的媒體類型,本例為"application/json" 或者 "application/xml"(在 Podcast 類前 添加 `@XmlRootElement` ),將返回 JSON 或者 XML 格式的 podcast 集合
響應
* 成功: HTTP 狀態 為 200 的 podcast
* 錯誤: 404 Not found。如果沒有在數據庫中找到
#### 3.2.3\. 更新 podcast
##### 3.2.3.1\. 設計
| **Description** | **URI** | **HTTP method
** | **HTTP Status response** |
| --- | --- | --- | --- |
| 更新 podcast (**完全**) | ?/podcasts/{id} | PUT | 200?OK |
| ?更新 podcast (**部分**) | ?/podcasts/{id} | POST | 200?OK |
1.完全更新 – 提供所有的值 2.部分更新 – 傳遞部分屬性值即可
##### 3.2.3.1\. 實現
###### 3.2.3.1.1\. 完全更新
創建或者完全更新資源
```
@PUT
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response putPodcastById(@PathParam("id") Long id, Podcast podcast)
throws AppException {
Podcast podcastById = podcastService.verifyPodcastExistenceById(id);
if (podcastById == null) {
// resource not existent yet, and should be created under the
// specified URI
Long createPodcastId = podcastService.createPodcast(podcast);
return Response
.status(Response.Status.CREATED)
// 201
.entity("A new podcast has been created AT THE LOCATION you specified")
.header("Location",
"http://localhost:8888/demo-rest-jersey-spring/podcasts/"
+ String.valueOf(createPodcastId)).build();
} else {
// resource is existent and a full update should occur
podcastService.updateFullyPodcast(podcast);
return Response
.status(Response.Status.OK)
// 200
.entity("The podcast you specified has been fully updated created AT THE LOCATION you specified")
.header("Location",
"http://localhost:8888/demo-rest-jersey-spring/podcasts/"
+ String.valueOf(id)).build();
}
}
```
注解
* `@PUT `– 指示方法響應到 HTTP PUT 請求
* `@PathParam("id")`- 綁定傳遞的參數值
* `@Consumes({MediaType.APPLICATION_JSON})` – 定義方法可以接受的媒體類型,本例為"application/json"
* `@Produces({MediaType.TEXT_HTML})` – 定義方法可以產生的媒體類型,本例為t"ext/html"
響應
* 創建
* 成功: HTTP 狀態 為 201 Created
* 錯誤: 400 Bad Request。如果需要的屬性值沒有提供
* 完全更新:
* 成功: HTTP 狀態 為 200
* 錯誤: 400 Bad Request。如果不是所有的屬性都提供
###### 3.2.3.1.2\. 部分更新
```
//PARTIAL update
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response partialUpdatePodcast(@PathParam("id") Long id, Podcast podcast) throws AppException {
podcast.setId(id);
podcastService.updatePartiallyPodcast(podcast);
return Response.status(Response.Status.OK)// 200
.entity("The podcast you specified has been successfully updated")
.build();
}
```
注解
* `@POST` – 指示方法響應到 HTTP POST 請求
* `@PathParam("id")`- 綁定傳遞的參數值
* `@Consumes({MediaType.APPLICATION_JSON})` – 定義方法可以接受的媒體類型,本例為"application/json"
* `@Produces({MediaType.TEXT_HTML})` – 定義方法可以產生的媒體類型,本例為t"ext/html"
響應
* 成功: HTTP 狀態 為 200 OK
* 錯誤: 404 Not Found。如果資源不存在
#### 3.2.4\. 刪除 podcast
##### 3.2.4.1\. 設計
| **Description** | **URI** | **HTTP method
** | **HTTP Status response** |
| --- | --- | --- | --- |
| 移除所有 podcasts | ?/podcasts/ | DELETE | 204 No content |
| 移除特定位置的 podcast | ?/podcasts/{id} | DELETE | 204 No content |
##### 3.2.4.2\. 實現
###### 3.2.4.2.1\. 刪除所有資源
```
@DELETE
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcasts() {
podcastService.deletePodcasts();
return Response.status(Response.Status.NO_CONTENT)// 204
.entity("All podcasts have been successfully removed").build();
}
```
注解
* `@DELETE` – 指示方法響應到 HTTP DELETE 請求
* `@Produces({MediaType.TEXT_HTML})` – 定義方法可以產生的媒體類型,本例為"text/html"
響應
* 返回 html 文檔
###### 3.2.4.2.2\. 刪除一個資源
```
@DELETE
@Path("{id}")
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcastById(@PathParam("id") Long id) {
podcastService.deletePodcastById(id);
return Response.status(Response.Status.NO_CONTENT)// 204
.entity("Podcast successfully removed from database").build();
}
```
注解
* `@DELETE` – 指示方法響應到 HTTP DELETE 請求
* `@PathParam("id")`- 綁定傳遞的參數值
* `@Consumes({MediaType.APPLICATION_JSON})` – 定義方法可以接受的媒體類型,本例為"application/json"
* `@Produces({MediaType.TEXT_HTML})` – 定義方法可以產生的媒體類型,本例為"text/html"
響應
* 成功: HTTP 狀態 為 204 No Content
* 錯誤: 404 Not Found。如果資源不存在
## 4\. 日志
詳見 [http://www.codingpedia.org/ama/how-to-log-in-spring-with-slf4j-and-logback/](http://www.codingpedia.org/ama/how-to-log-in-spring-with-slf4j-and-logback/)
## 5\. 異常處理
錯誤處理要有統一的格式,就像下面
```
{
"status": 400,
"code": 400,
"message": "Provided data not sufficient for insertion",
"link": "http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-with-jersey-and-spring",
"developerMessage": "Please verify that the feed is properly generated/set"
}
```
## 6\. 服務端添加 CORS 支持
## 7\. 測試
### 7.1\. 在Java集成測試
### 7.1.1\. 配置
##### 7.1.1.1 Jersey 客戶端依賴
```
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
```
##### 7.1.1.2\. Failsafe 插件
```
<plugins>
[...]
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.16</version>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
[...]
</plugins>
```
##### 7.1.1.2\. Jetty Maven 插件
```
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<jettyConfig>${project.basedir}/src/main/resources/config/jetty9.xml</jettyConfig>
<stopKey>STOP</stopKey>
<stopPort>9999</stopPort>
<stopWait>5</stopWait>
<scanIntervalSeconds>5</scanIntervalSeconds>
[...]
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<!-- stop any previous instance to free up the port -->
<goal>stop</goal>
<goal>run-exploded</goal>
</goals>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
[...]
</plugins>
```
詳細配置見源碼中的 pom.xml
#### 7.1.2\. 編譯集成測試
使用 JUnit 作為測試框架。默認的 Failsafe 插件 自動包含所有測試類
* "`_*/IT_.java`" – “IT”開頭的文件.
* "`_*/_IT.java`" – “IT”結尾的文件.
* "`_*/_ITCase.java`" – “ITCase”結尾的文件.
創建了測試類 RestDemoServiceIT
```
public class RestDemoServiceIT {
[....]
@Test
public void testGetPodcast() throws JsonGenerationException,
JsonMappingException, IOException {
ClientConfig clientConfig = new ClientConfig();
clientConfig.register(JacksonFeature.class);
Client client = ClientBuilder.newClient(clientConfig);
WebTarget webTarget = client
.target("http://localhost:8888/demo-rest-jersey-spring/podcasts/2");
Builder request = webTarget.request(MediaType.APPLICATION_JSON);
Response response = request.get();
Assert.assertTrue(response.getStatus() == 200);
Podcast podcast = response.readEntity(Podcast.class);
ObjectMapper mapper = new ObjectMapper();
System.out
.print("Received podcast from database *************************** "
+ mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(podcast));
}
}
```
注意:
* 在客戶也要注冊 `JacksonFeature` ,這樣才能解析 JSON格式
* 用 jetty 測試,端口 8888
* 期望 返回 200 狀態 給我們的請求
* `org.codehaus.jackson.map.ObjectMapper` 幫助返回格式化的 JSON
#### 7.1.3\. 運行集成測試
運行
```
mvn verify
```
設置 `jetty.port` 屬性到 8888,Eclipse 配置如下
### 7.2\. 用 SoapUI 集成測試
[youtube視頻教程](http://www.youtube.com/watch?v=XV7WW0bDy9c)(需翻墻)
## 8\. 版本管理
幾個要點:
* `URL: “/v1/podcasts/{id}”`
* `Accept/Content-type header: application/json; version=1`
在 路徑中 加入 版本信息
```
@Component
@Path("/v1/podcasts")
public class PodcastResource {...}
```
參考:
* [https://jersey.java.net/](https://jersey.java.net/)
* [https://github.com/waylau/Jersey-2.x-User-Guide](https://github.com/waylau/Jersey-2.x-User-Guide)
* [http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/](http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/)
- 用Jersey構建RESTful服務
- 用Jersey構建RESTful服務1--HelloWorld
- 用Jersey構建RESTful服務2--JAVA對象轉成XML輸出
- 用Jersey構建RESTful服務3--JAVA對象轉成JSON輸出
- 用Jersey構建RESTful服務4--通過jersey-client客戶端調用Jersey的Web服務模擬CURD
- 用Jersey構建RESTful服務5--Jersey+MySQL5.6+Hibernate4.3
- 用Jersey構建RESTful服務6--Jersey+SQLServer+Hibernate4.3
- 用Jersey構建RESTful服務7--Jersey+SQLServer+Hibernate4.3+Spring3.2
- 用Jersey構建RESTful服務8--Jersey+SQLServer+Hibernate4.3+Spring3.2+jquery
- 用Jersey構建RESTful服務9--Jersey+SQLServer+Hibernate4.3+Spring3.2+AngularJS
- 用 Jersey 2 和 Spring 4 構建 RESTful web service