REST 簡介
REST 是英文 Representational State Transfer 的縮寫,有中文翻譯為“具象狀態傳輸”。REST 這個術語是由 Roy Fielding 在他的博士論文 《 Architectural Styles and the Design of Network-based Software Architectures 》中提出的。REST 並非標准,而是一種開發 Web 應用的架構風格,可以將其理解為一種設計模式。REST 基於 HTTP,URI,以及 XML 這些現有的廣泛流行的協議和標准,伴隨著 REST,HTTP 協議得到了更加正確的使用。
相較於基於 SOAP 和 WSDL 的 Web 服務,REST 模式提供了更為簡潔的實現方案。目前,越來越多的 Web 服務開始采用 REST 風格設計和實現,真實世界中比較著名的 REST 服務包括:Google AJAX 搜索 API、Amazon Simple Storage Service (Amazon S3) 等。
基於 REST 的 Web 服務遵循一些基本的設計原則:
系統中的每一個對象或是資源都可以通過一個唯一的 URI 來進行尋址,URI 的結構應該簡單、可預測且易於理解,比如定義目錄結構式的 URI。
以遵循 RFC-2616 所定義的協議的方式顯式地使用 HTTP 方法,建立創建、檢索、更新和刪除(CRUD:Create, Retrieve, Update and Delete)操作與 HTTP 方法之間的一對一映射:
若要在服務器上創建資源,應該使用 POST 方法;
若要檢索某個資源,應該使用 GET 方法;
若要更改資源狀態或對其進行更新,應該使用 PUT 方法;
若要刪除某個資源,應該使用 DELETE 方法。
URI 所訪問的每個資源都可以使用不同的形式加以表示(比如 XML 或者 JSON),具體的表現形式取決於訪問資源的客戶端,客戶端與服務提供者使用一種內容協商的機制(請求頭與 MIME 類型)來選擇合適的數據格式,最小化彼此之間的數據耦合。
JAX-RS -- Java API for RESTful Web Services
Java EE 6 引入了對 JSR-311 的支持。JSR-311(JAX-RS:Java API for RESTful Web Services)旨在定義一個統一的規范,使得 Java 程序員可以使用一套固定的接口來開發 REST 應用,避免了依賴於第三方框架。同時,JAX-RS 使用 POJO 編程模型和基於標注的配置,並集成了 JAXB,從而可以有效縮短 REST 應用的開發周期。
JAX-RS 定義的 API 位於 javax.ws.rs 包中,其中一些主要的接口、標注和抽象類如 圖 1 所示。
圖 1. javax.ws.rs 包概況
JAX-RS 的具體實現由第三方提供,例如 Sun 的參考實現 Jersey、Apache 的 CXF 以及 JBoss 的 RESTEasy。
在接下來的文章中,將結合一個記賬簿應用向讀者介紹 JAX-RS 一些關鍵的細節。
示例簡介
記賬簿示例應用程序中包含了 3 種資源:賬目、用戶以及賬目種類,用戶與賬目、賬目種類與賬目之間都是一對多的關系。記賬簿實現的主要功能包括:
記錄某用戶在什麼時間花費了多少金額在哪個種類上
按照用戶、賬目種類、時間或者金額查詢記錄
對用戶以及賬目種類的管理
Resource 類和 Resource 方法
Web 資源作為一個 Resource 類來實現,對資源的請求由 Resource 方法來處理。Resource 類或 Resource 方法被打上了 Path 標注,Path 標注的值是一個相對的 URI 路徑,用於對資源進行定位,路徑中可以包含任意的正則表達式以匹配資源。和大多數 JAX-RS 標注一樣,Path 標注是可繼承的,子類或實現類可以繼承超類或接口中的 Path 標注。
Resource 類是 POJO,使用 JAX-RS 標注來實現相應的 Web 資源。Resource 類分為根 Resource 類和子 Resource 類,區別在於子 Resource 類沒有打在類上的 Path 標注。Resource 類的實例方法打上了 Path 標注,則為 Resource 方法或子 Resource 定位器,區別在於子 Resource 定位器上沒有任何 @GET、@POST、@PUT、@DELETE 或者自定義的 @HttpMethod。清單 1 展示了示例應用中使用的根 Resource 類及其 Resource 方法。
清單 1. 根 Resource 類
@Path("/")
public class BookkeepingService {
......
@Path("/person/")
@POST
@Consumes("application/json")
public Response createPerson(Person person) {
......
}
@Path("/person/")
@PUT
@Consumes("application/json")
public Response updatePerson(Person person) {
......
}
@Path("/person/{id:\\d+}/")
@DELETE
public Response deletePerson(@PathParam("id")
int id) {
......
}
@Path("/person/{id:\\d+}/")
@GET
@Produces("application/json")
public Person readPerson(@PathParam("id")
int id) {
......
}
@Path("/persons/")
@GET
@Produces("application/json")
public Person[] readAllPersons() {
......
}
@Path("/person/{name}/")
@GET
@Produces("application/json")
public Person readPersonByName(@PathParam("name")
String name) {
......
}
......
參數標注
JAX-RS 中涉及 Resource 方法參數的標注包括:@PathParam、@MatrixParam、@QueryParam、@FormParam、@HeaderParam、 @CookieParam、@DefaultValue 和 @Encoded。這其中最常用的是 @PathParam,它用於將 @Path 中的模板變量映射到方法參數,模板變量支持使用正則表達式,變量名與正則表達式之間用分號分隔。例如對 清單 1 中所示的 BookkeepingService 類,如果使用 Get 方法請求資源”/person/jeffyin”,則 readPersonByName 方法將被調用,方法參數 name 被賦值為”jeffyin”;而如果使用 Get 方法請求資源”/person/123”,則 readPerson 方法將被調用,方法參數 id 被賦值為 123。要了解如何使用其它的參數標注 , 請參考 JAX-RS API。
JAX-RS 規定 Resource 方法中只允許有一個參數沒有打上任何的參數標注,該參數稱為實體參數,用於映射請求體。例如 清單 1中所示的 BookkeepingService 類的 createPerson 方法和 updatePerson 方法的參數 person。
參數與返回值類型
Resource 方法合法的參數類型包括:
原生類型
構造函數接收單個字符串參數或者包含接收單個字符串參數的靜態方法 valueOf 的任意類型
List<T>,Set<T>,SortedSet<T>(T 為以上的 2 種類型)
用於映射請求體的實體參數
Resource 方法合法的返回值類型包括:
void:狀態碼 204 和空響應體
Response:Response 的 status 屬性指定了狀態碼,entity 屬性映射為響應體
GenericEntity:GenericEntity 的 entity 屬性映射為響應體,entity 屬性為空則狀態碼為 204,非空則狀態碼為 200
其它類型:返回的對象實例映射為響應體,實例為空則狀態碼為 204,非空則狀態碼為 200
對於錯誤處理,Resource 方法可以拋出非受控異常 WebApplicationException 或者返回包含了適當的錯誤碼集合的 Response 對象。
Context 標注
通過 Context 標注,根 Resource 類的實例字段可以被注入如下類型的上下文資源:
Request、UriInfo、HttpHeaders、Providers、SecurityContext
HttpServletRequest、HttpServletResponse、ServletContext、ServletConfig
CRUD 操作
JAX-RS 定義了 @POST、@GET、@PUT 和 @DELETE,分別對應 4 種 HTTP 方法,用於對資源進行創建、檢索、更新和刪除的操作。
POST 標注
POST 標注用於在服務器上創建資源,如 清單 2 所示。
清單 2. POST 標注
@Path("/")
public class BookkeepingService {
......
@Path("/account/")
@POST
@Consumes("application/json")
public Response createAccount(Account account) {
......
}
......
如果使用 POST 方法請求資源”/account”,則 createAccount 方法將被調用,JSON 格式的請求體被自動映射為實體參數 account。
GET 標注
GET 標注用於在服務器上檢索資源,如 清單 3 所示。
清單 3. GET 標注
@Path("/")
public class BookkeepingService {
......
@Path("/person/{id}/accounts/")
@GET
@Produces("application/json")
public Account[] readAccountsByPerson(@PathParam("id")
int id) {
......
}
......
@Path("/accounts/{beginDate:\\d{4}-\\d{2}-\\d{2}},{endDate:\\d{4}-\\d{2}-\\d{2}}/")
@GET
@Produces("application/json")
public Account[] readAccountsByDateBetween(@PathParam("beginDate")
String beginDate, @PathParam("endDate")
String endDate) throws ParseException {
......
}
......
如果使用 GET 方法請求資源”/person/123/accounts”,則 readAccountsByPerson 方法將被調用,方法參數 id 被賦值為 123,Account 數組類型的返回值被自動映射為 JSON 格式的響應體;而如果使用 GET 方法請求資源”/accounts/2008-01-01,2009-01-01”,則 readAccountsByDateBetween 方法將被調用,方法參數 beginDate 被賦值為”2008-01-01”,endDate 被賦值為”2009-01-01”,Account 數組類型的返回值被自動映射為 JSON 格式的響應體。
PUT 標注
PUT 標注用於更新服務器上的資源,如 清單 4 所示。
清單 4. PUT 標注
@Path("/")
public class BookkeepingService {
......
@Path("/account/")
@PUT
@Consumes("application/json")
public Response updateAccount(Account account) {
......
}
......
如果使用 PUT 方法請求資源”/account”,則 updateAccount 方法將被調用,JSON 格式的請求體被自動映射為實體參數 account。
DELETE 標注
DELETE 標注用於刪除服務器上的資源,如 清單 5 所示。
清單 5. DELETE 標注
@Path("/")
public class BookkeepingService {
......
@Path("/account/{id:\\d+}/")
@DELETE
public Response deleteAccount(@PathParam("id")
int id) {
......
}
......
如果使用 DELETE 方法請求資源”/account/323”,則 deleteAccount 方法將被調用,方法參數 id 被賦值為 323。
內容協商與數據綁定
Web 資源可以有不同的表現形式,服務端與客戶端之間需要一種稱為內容協商(Content Negotiation)的機制:作為服務端,Resource 方法的 Produces 標注用於指定響應體的數據格式(MIME 類型),Consumes 標注用於指定請求體的數據格式;作為客戶端,Accept 請求頭用於選擇響應體的數據格式,Content-Type 請求頭用於標識請求體的數據格式。
JAX-RS 依賴於 MessageBodyReader 和 MessageBodyWriter 的實現來自動完成返回值到響應體的序列化以及請求體到實體參數的反序列化工作,其中,XML 格式的請求/響應數據與 Java 對象的自動綁定依賴於 JAXB 的實現。
用戶可以使用 Provider 標注來注冊使用自定義的 MessageBodyProvider,如 清單 6 所示,GsonProvider 類使用了 Google Gson 作為 JSON 格式的 MessageBodyProvider 的實現。
清單 6. GsonProvider
@Provider
@Produces("application/json")
@Consumes("application/json")
public class GsonProvider implements MessageBodyWriter<Object>,
MessageBodyReader<Object> {
private final Gson gson;
public GsonProvider() {
gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().setDateFormat(
"yyyy-MM-dd").create();
}
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
return true;
}
public Object readFrom(Class<Object> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
return gson.fromJson(new InputStreamReader(entityStream, "UTF-8"), type);
}
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
return true;
}
public long getSize(Object obj, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
public void writeTo(Object obj, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
entityStream.write(gson.toJson(obj, type).getBytes("UTF-8"));
}
}
JAX-RS 與 JPA 的結合使用
由於 JAX-RS 和 JPA 同樣都使用了基於 POJO 和標注的編程模型,因而很易於結合在一起使用。示例應用中的 Web 資源 ( 如賬目 ) 同時也是持久化到數據庫中的實體,同一個 POJO 類上既有 JAXB 的標注,也有 JPA 的標注 ( 或者還有 Gson 的標注 ) ,這使得應用中類的個數得以減少。如 清單 7 所示,Account 類可以在 JAX-RS 與 JPA 之間得到復用,它不但可以被 JAX-RS 綁定為請求體 / 響應體的 XML/JSON 數據,也可以被 JPA 持久化到關系型數據庫中。
清單 7. Account
@Entity
@Table(name = "TABLE_ACCOUNT")
@XmlRootElement
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COL_ID")
@Expose
private int id;
@ManyToOne
@JoinColumn(name = "COL_PERSON")
@Expose
private Person person;
@Column(name = "COL_AMOUNT")
@Expose
private BigDecimal amount;
@Column(name = "COL_DATE")
@Expose
private Date date;
@ManyToOne
@JoinColumn(name = "COL_CATEGORY")
@Expose
private Category category;
@Column(name = "COL_COMMENT")
@Expose
private String comment;
......
結束語
REST 作為一種輕量級的 Web 服務架構被越來越多的開發者所采用,JAX-RS 的發布則規范了 REST 應用開發的接口。本文首先闡述了 REST 架構的基本設計原則,然後通過一個示例應用展示了 JAX-RS 是如何通過各種標注來實現以上的設計原則的,最後還介紹了 JAX-RS 與 JPA、Gson 的結合使用。本文的示例應用使用了 Jersey 和 OpenJPA,部署在 Tomcat 容器上,替換成其它的實現只需要修改 web.xml 和 persistence.xml 配置文件。
本文配套源碼