1、導言
過去幾年,REST逐漸成為影響Web架構、Web協定與Web應用設計的重要概念。如果你還不了解REST,那這個簡短的介紹将有助你快速掌握REST,此外還可以點選這裡了解關于REST的更多資訊。現在有越來越多的公司希望能以簡單而又貼合Web架構本身的方式公開Web API,是以REST變得越來越重要也就不足為奇了。使用Ajax進行通信的富浏覽器端也在朝這個目标不斷邁進。這個架構原則提升了網際網路的可伸縮性,無論何種應用都能從該原則中受益無窮。
JAX-RS(JSR 311)指的是Java API for RESTful Web Services,Roy Fielding也參與了JAX-RS的制訂,他在自己的博士論文中定義了REST。對于那些想要建構RESTful Web Services的開發者來說,JAX-RS給出了不同于JAX-WS(JSR-224)的另一種解決方案。目前共有4種JAX-RS實作,所有這些實作都支援Spring,Jersey則是JAX-RS的參考實作,也是本文所用的實作。
本文将介紹Spring 3中的REST特性并與JAX-RS進行對比,希望能幫助你理順這兩種程式設計模型之間的異同點。如果你使用Spring進行開發,那可能想知道(或者有人曾問過你)Spring MVC與JAX-RS有何異同點?更進一步,如果你手頭有一個Spring MVC應用,使用了控制類繼承(SimpleFormController等),你可能還意識不到現在的Spring MVC對REST廣泛的支援。
開始前,有必要指出JAX-RS的目标是Web Services開發(這與HTML Web應用不同),而Spring MVC的目标則是Web應用開發。Spring 3為Web應用與Web Services增加了廣泛的REST支援,但本文則關注于與Web Services開發相關的特性。我覺得這種方式更有助于在JAX-RS的上下文中讨論Spring MVC。要說明的第二點是,我們将要讨論的REST特性是Spring Framework的一部分,也是現有的Spring MVC程式設計模型的延續,是以,并沒有所謂的“Spring REST framework”這種概念,有的隻是Spring和Spring MVC。這意味着如果你有一個Spring應用的話,你既可以使用Spring MVC建立HTML Web層,也可以建立RESTful Web Services層。
注:關于文中的代碼片段,假想了一個簡單的領域模型:兩個JPA注解實體,分别是Account和Portfolio,其中一個Account對應多個Portfolio。持久層使用Spring配置,包含了一個JPA倉儲實作,用于擷取和持久化實體執行個體。Jersey和Spring MVC用于建構Web Services層,通過調用底層的Spring托管應用來服務用戶端請求。
2、引導程式與Web層包裝
我們會在Spring MVC和JAX-RS中都使用Spring實作依賴注入。Spring MVC DispatcherServlet和Jersey SpringServlet會把請求,代理給Spring管理的REST層元件(控制器或資源),後者會由業務或持久層元件包裝起來,如下圖所示:

Jersey和Spring MVC都使用Spring的ContextLoaderListener加載業務與持久層元件,比如JpaAccountRepository。ContextLoaderListener可用于任何Web或REST架構環境中。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:META-INF/spring/module-config.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
3、在Jersey中建立Spring管理的JAX-RS資源
Jersey支援在REST層中使用Spring,兩個簡單的步驟就能搞定(事實上有3步,還需要将建構依賴加到maven artifact com.sun.jersey.contribs:jersey-spring中)。
步驟一:将如下配置片段加到web.xml中以保證Spring能夠建立JAX-RS根資源:
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>
com.sun.jersey.spi.spring.container.servlet.SpringServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
步驟二:使用Spring和JAX-RS注解聲明根JAX-RS資源類:
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@Context
UriInfo uriInfo;
@Autowired
private AccountRepository accountRepository;
}
如下是對這些注解的說明: @Component将AccountResource聲明為Spring bean。 @Scope聲明了一個prototype Spring bean,這樣每次使用時都會執行個體化(比如每次請求時)。 @Autowired指定了一個AccountRepository引用,Spring會提供該引用。
@Path是個JAX-RS注解,它将AccountResource聲明為“根”JAX-RS資源。 @Context也是一個JAX-RS注解,要求注入特定于請求的UriInfo對象。 JAX-RS有“根”資源(标記為@Path)和子資源的概念。在上面的示例中,AccountResource就是個根資源,它會處理以“/accounts/”開頭的路徑。AccountResource中的方法如getAccount()隻需聲明針對類型級别的相對路徑即可。
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
}
}
通路路徑“/accounts/{username}”(其中的username是路徑參數,可以是某個賬戶的使用者名)的請求将由getAccount()方法處理。 根資源由JAX-RS運作時(在本示例中是Spring)執行個體化,子資源則由應用本身執行個體化。比如說,對于“/accounts/{username}/portfolios/{portfolioName}”這樣的請求,AccountResource(由路徑的第一部分“/accounts”辨別)會建立一個子資源執行個體,請求會被代理給該執行個體。
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@Path("{username}/portfolios/")
public PortfolioResource getPortfolioResource(@PathParam("username") String username) {
return new PortfolioResource(accountRepository, username, uriInfo);
}
}
PortfolioResource本身的聲明并沒有使用注解,是以,其所有的依賴都是由父資源傳遞過來的:
public class PortfolioResource {
private AccountRepository accountRepository;
private String username;
private UriInfo uriInfo;
public PortfolioResource(AccountRepository accountRepository, String username, UriInfo uriInfo) {
this.accountRepository = accountRepository;
this.username = username;
this.uriInfo = uriInfo;
}
}
JAX-RS中的根與子資源建立了一個處理鍊,它會調用多個資源:
請記住,資源類是Web Services層元件,應當關注于Web Services相關的處理,比如輸入轉換、準備響應、設定響應代碼等等。此外,将Web Services邏輯與業務邏輯分隔開來的實踐需要将業務邏輯包裝到單獨的方法中以作為事務邊界。
4、建立Spring MVC @Controller類
對于Spring MVC來說,我們需要建立DispatcherServlet,同時将contextConfigLocation參數指定為Spring MVC配置:
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*.xml
</param-value>
</init-param>
</servlet>
要想在Spring MVC(@MVC)中使用基于注解的程式設計模型還需要少量的配置。下面的component-scan元素會告訴Spring去哪裡尋找@Controller注解類。
<context:component-scan base-package="org.springframework.samples.stocks" />
接下來,我們聲明了AccountController,如下代碼所示:
@Controller
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountRepository accountRepository;
}
@RequestMapping注解會将該控制器映射到所有以“/accounts”開頭的請求上。AccountController中的方法如getAccount()隻需聲明針對“/accounts”的相對位址即可。
@RequestMapping(value = "/{username}", method = GET)
public Account getAccount(@PathVariable String username) {
}
Spring MVC則沒有根資源與子資源的概念,這樣每個控制器都是由Spring而非應用來管理的:
@Controller
@RequestMapping("/accounts/{username}/portfolios")
public class PortfolioController {
@Autowired
private AccountRepository accountRepository;
}
對“/accounts/{username}/portfolios”的請求會被直接代理給PortfolioController,AccountController則完全不會參與其中。需要注意的是,該請求也可以直接由AccountController處理,這樣就不需要PortfolioController了。
5、Web層元件範圍
在JAX-RS中,AccountResource是通過前請求(per-request)語義聲明的,這也是JAX-RS預設的推薦設定。這麼做可以将特定于請求的資料注入并存儲到資源類本身當中,這适用于由JAX-RS所管理的根級别資源。子資源由應用執行個體化,并不會直接從這種方法中獲益。在Spring MVC中,控制器永遠都是單例的,他們将特定于請求的資料作為方法參數。JAX-RS也可以這麼做,以單例的方式建立資源。
6、将請求映射到方法上
接下來,我們看看Spring MVC和JAX-RS如何将請求映射到方法上。@Path和@RequestMapping都可以從URL中抽取出路徑變量:
@Path("/accounts/{username}")
@RequestMapping("/accounts/{username}")
這兩個架構也都可以使用正規表達式抽取路徑變量:
@Path("/accounts/{username:.*}")
@RequestMapping("/accounts/{username:.*}"
Spring MVC的@RequestMapping可以根據查詢參數的有無來比對請求,或是根據查詢參數值進行比對:
@RequestMapping(parameters="foo")
@RequestMapping(parameters="!foo")
@RequestMapping(parameters="foo=123")
@RequestMapping還可以根據頭資訊的有無來比對請求或是根據頭資訊的值進行比對:
@RequestMapping(headers="Foo-Header")
@RequestMapping(headers="!Foo-Header")
@RequestMapping(headers="content-type=text/*")
7、處理請求資料
HTTP請求中包含着應用需要提取和處理的資料,如HTTP頭、cookie、查詢字元串參數、表單參數以及請求體(XML、JSON等)中所包含的大量資料。在RESTful應用中,URL本身也可以帶有重要的資訊,如通過路徑參數指定需要通路哪個資源、通過檔案擴充名(.html, .pdf)指定需要何種内容類型等。HttpServletRequest提供了處理這一切的所有底層通路機制,但直接使用HttpServletRequest實在是太乏味了。
1、請求參數、Cookies和HTTP頭
Spring MVC和JAX-RS擁有能夠抽取這種HTTP請求值的注解:
@GET
@Path
public void foo(@QueryParam("q") String q, @FormParam("f") String f, @CookieParam("c") String c,
@HeaderParam("h") String h, @MatrixParam("m") m) {
// JAX-RS
}
@RequestMapping(method=GET)
public void foo(@RequestParam("q") String q, @CookieValue("c") String c, @RequestHeader("h") String h) {
// Spring MVC
}
上面的注解非常像,差別在于JAX-RS支援矩陣參數(matrix parameters)的抽取,擁有單獨的注解來處理查詢字元串和表單參數。矩陣參數并不常見,他們類似于查詢字元串參數,但卻使用了特殊的路徑片段(比如GET /images;name=foo;type=gif)。稍後将介紹表單參數。
假如使用了前請求範圍聲明資源,那麼JAX-RS可以在屬性和setters方法上使用上述注解。
Spring MVC有個特性能讓我們少敲幾個字元,如果注解名與Java參數名相同,那麼就可以省略掉上面的注解名了。比如說,名為“q”的請求參數要求方法參數也得為“q”:
public void foo(@RequestParam String q, @CookieValue c, @RequestHeader h) {
}
這對于那些在參數中使用了注解而導緻方法簽名變長的情況來說實在是太友善了。請記住,這個特性要求代碼使用調試符号進行編譯。
2、類型轉換與HTTP請求值的格式化
HTTP請求值(頭、cookies和參數)是不變的字元串并且需要解析。
JAX-RS通過尋找valueOf()方法或是在客戶化的目标類型中接收字元串的構造方法來解析請求資料。JAX-RS支援如下類型的注解方法參數,包括路徑變量、請求參數、HTTP頭值和cookies:
原生類型、
擁有接收單個字元串參數的構造方法的類型。
擁有一個接收單個字元串參數的名為valueOf的靜态方法的類型。
List<T>、Set<T>或是SortedSet<T>,其中的T滿足上面2個或3個要求。
Spring 3支援上面所有要求。除此之外,Spring 3提供了一種全新的類型轉換與格式化機制,并且可以使用注解實作。
3、表單資料
如前所述,JAX-RS處理查詢字元串參數和表單參數的方式是不同的。雖然Spring MVC隻有一個@RequestParam,但它還提供了一種Spring MVC使用者很熟悉的資料綁定機制來處理表單輸入。
比如說,如果一個表單送出了3個資料,那麼一種可能的處理方式就是聲明一個帶有3個參數的方法:
@RequestMapping(method=POST)
public void foo(@RequestParam String name, @RequestParam creditCardNumber, @RequestParam expirationDate) {
Credit card = new CreditCard();
card.setName(name);
card.setCreditCardNumber(creditCardNumber);
card.setExpirationDate(expirationDate);
}
然而,随着表單資料量的增加,這種處理方式就會變得不切實際。借助于資料綁定,Spring MVC可以建立、組裝并傳遞包含有嵌套資料(賬單位址、郵件位址等)、任意結構的表單對象。
@RequestMapping(method=POST)
public void foo(CreditCard creditCard) {
// POST /creditcard/1
// name=Bond
// creditCardNumber=1234123412341234
// expiration=12-12-2012
}
要想與Web浏覽器協同工作,表單處理是個重要環節。另一方面,Web Services用戶端一般會在請求體中送出XML或JSON格式的資料。
4、處理請求體中的資料
無論是Spring MVC還是JAX-RS都能夠自動處理請求體中的資料:
@POST
public Response createAccount(Account account) {
// JAX_RS
}
@RequestMapping(method=POST)
public void createAccount(@RequestBody Account account) {
// Spring MVC
}
JAX-RS中的請求體資料:
在JAX-RS中,類型MessageBodyReader的實體供應者負責轉換請求體資料。JAX-RS的實作需要擁有一個JAXB MessageBodyReader,這可以使用具有注解@Provider的客戶化MessageBodyReader實作。
Spring MVC中的請求體資料:
在Spring MVC中,如果想通過請求體資料初始化方法參數,那可以将@RequestBody注解加到該方法參數前,這與之前介紹的表單參數初始化正好相反。在Spring MVC中,HttpMessageConverter類負責轉換請求體資料,Spring MVC提供了一個開箱即用的Spring OXM HttpMessageConverter。它支援JAXB、Castor、JiBX、XMLBeans和XStream,此外還有一個用于處理JSON的Jackson HttpMessageConverter。HttpMessageConverter會注冊到AnnotationMethodHandlerAdapter上,後者會将到來的請求映射到Spring MVC @Controllers上。下面是其配置:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
<property name="messageConverters" ref="marshallingConverter"/>
</bean>
<bean id="marshallingConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
<constructor-arg ref="jaxb2Marshaller"/>
<property name="supportedMediaTypes" value="application/vnd.stocks+xml"/>
</bean>
<oxm:jaxb2-marshaller id="jaxb2Marshaller"/>
下圖闡述了該配置:
Spring 3新增的mvc客戶化命名空間将上述配置自動化了,隻需增加如下配置片段即可:
<mvc:annotation-driven />
如果JAXB位于類路徑上,它會注冊一個用于讀寫XML的轉換器;如果Jackson位于類路徑上,它會注冊一個用于讀寫JSON的轉換器。
8、準備響應
典型的響應需要準備響應代碼、設定HTTP響應頭、将資料放到響應體當中,還需要處理異常。
1、使用JAX-RS設定響應體資料
在JAX-RS中,要想将資料加到響應體中,隻需要從資源方法中傳回對象即可:
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
return accountRepository.findAccountByUsername(username);
}
JAX-RS會尋找類型MessageBodyWriter的實體供應者,它能将對象轉換為所需的内容類型。JAX-RS實作需要具備一個JAXB MessageBodyWriter,這可以使用具有注解@Provider的客戶化MessageBodyWriter實作。
2、使用Spring MVC設定響應體資料
在Spring MVC中,響應是通過一個視圖解析過程來實作的,這樣就可以從一系列視圖技術中選擇了。但在與Web Services用戶端互動時,更加合理的方式則是舍棄視圖解析過程,轉而使用方法所傳回的對象:
@RequestMapping(value="/{username}", method=GET)
public @ResponseBody Account getAccount(@PathVariable String username) {
return accountRepository.findAccountByUsername(username);
}
如果對控制器方法或其傳回類型應用注解@ResponseBody,那麼就會使用HttpMessageConverter處理傳回值,然後用該傳回值設定響應體。用于請求體參數的HttpMessageConverter集合也用于響應體,是以無需再做任何配置。
3、狀态代碼與響應頭
JAX-RS使用一個鍊式API來建構響應:
@PUT @Path("{username}")
public Response updateAccount(Account account) {
// ...
return Response.noContent().build(); // 204 (No Content)
}
這可以與UriBuilder聯合使用來為Location響應頭建立實體連結:
@POST
public Response createAccount(Account account) {
// ...
URI accountLocation = uriInfo.getAbsolutePathBuilder().path(account.getUsername()).build();
return Response.created(accountLocation).build();
}
上面代碼中所用的uriInfo要麼被注入到根資源(使用了@Context)中,要麼是從父資源傳遞給子資源。它可以附加到目前請求的路徑之後。
Spring MVC提供了一個注解來設定響應代碼:
@RequestMapping(method=PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateAccount(@RequestBody Account account) {
// ...
}
可以直接使用HttpServletResponse對象設定Location頭:
@RequestMapping(method=POST)
@ResponseStatus(CREATED)
public void createAccount(@RequestBody Account account, HttpServletRequest request,
HttpServletResponse response) {
// ...
String requestUrl = request.getRequestURL().toString();
URI uri = new UriTemplate("{requestUrl}/{username}").expand(requestUrl, account.getUsername());
response.setHeader("Location", uri.toASCIIString());
}
9、異常處理
JAX-RS允許資源方法抛出WebApplicationException類型的異常,該異常會包含一個響應。下面的示例代碼将一個JPA NoResultException轉換為特定于Jersey的NotFoundException,這會導緻一個404的錯誤:
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
try {
return accountRepository.findAccountByUsername(username);
} catch (NoResultException e) {
throw new NotFoundException();
}
}
WebApplicationException執行個體會封裝必要的邏輯來生成特定的響應,但每個獨立的資源類方法中都需要捕獲異常。
Spring MVC支援定義控制器級别的方法來處理異常:
@Controller
@RequestMapping("/accounts")
public class AccountController {
@ResponseStatus(NOT_FOUND)
@ExceptionHandler({NoResultException.class})
public void handle() {
// ...
}
}
如果任何控制器方法抛出了JPA的NoResultException異常,上面的處理器方法就會得到調用并處理該異常,然後傳回一個404錯誤。這樣,每個控制器就都能處理異常了,好象來自同一個地方一樣。
10、總結
希望本文有助于你了解Spring MVC建構RESTful Web Services的方式及其與JAX-RS程式設計模型之間的異同點。
如果你是個Spring MVC使用者,那麼你可能用它開發過HTML Web應用了。REST概念适用于Web Services和Web應用,尤其是富用戶端互動上更是如此。除了本文介紹的特性之外,Spring 3還增加了對RESTful Web應用的支援。這是部分新特性的清單:用于從URL模闆建構URL的新的JSP客戶化标簽、基于HTTP PUT和DELETE模拟表單送出的Servlet過濾器、根據内容類型自動選擇視圖的ContentTypeNegotiatingViewResolver、新的視圖實作等等。此外,Spring文檔也改進頗多。