導言
過去幾年,REST逐漸成為影響Web框架、Web協議與Web應用設計的重要概念。
現在有越來越多的公司希望能以簡單而又貼合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進行開發,那可能想知道(或者有人曾問過你)Spring MVC與JAX-RS有何異同點? 更進一步,如果你手頭有一個Spring MVC應用,使用了控制類繼承(SimpleFormController等),你可能 還意識不到現在的Spring MVC對REST廣泛的支持。
本文將介紹Spring 3中的REST特性並與JAX-RS進行對比,希望能幫助你理順這兩種編程模型之間的異 同點。
開始前,有必要指出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托管應用來服務客戶端請 求。
引導程序與Web層包裝
我們會在Spring MVC和JAX-RS中都使用Spring實現依賴注入。Spring MVC DispatcherServlet和 Jersey SpringServlet會把請求代理給Spring管理的REST層組件(控制器或資源),後者會由業務或持久 層組件包裝起來,如下圖所示:
Jersey和Spring MVC都使用Spring的ContextLoaderListener加載業務與持久層組件,比如 JpaAccountRepository:
<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>
ContextLoaderListener可用於任何Web或REST框架環境中。
在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邏輯與業務邏輯分隔開來的實踐需要將業務邏輯包裝到 單獨的方法中以作為事務邊界。
創建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了。
Web層組件范圍
在JAX-RS中,AccountResource是通過前請求(per-request)語義聲明的,這也是JAX-RS默認的推薦 設置。這麼做可以將特定於請求的數據注入並存儲到資源類本身當中,這適用於由JAX-RS所管理的根級別 資源。子資源由應用實例化,並不會直接從這種方法中獲益。
在Spring MVC中,控制器永遠都是單例的,他們將特定於請求的數據作為方法參數。JAX-RS也可以這 麼做,以單例的方式創建資源。
將請求映射到方法上
接下來,我們看看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/*")
處理請求數據
HTTP請求中包含著應用需要提取和處理的數據,如HTTP頭、cookie、查詢字符串參數、表單參數以及 請求體(XML、JSON等)中所包含的大量數據。在RESTful應用中,URL本身也可以帶有重要的信息,如通 過路徑參數指定需要訪問哪個資源、通過文件擴展名(.html, .pdf)指定需要何種內容類型等。 HttpServletRequest提供了處理這一切的所有底層訪問機制,但直接使用 HttpServletRequest實在是太 乏味了。
請求參數、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) {
}
這對於那些在參數中使用了注解而導致方法簽名變長的情況來說實在是太方便了。請記住,這個特性 要求代碼使用調試符號進行編譯。
類型轉換與HTTP請求值的格式化
HTTP請求值(頭、cookies和參數)是不變的字符串並且需要解析。
JAX-RS通過尋找valueOf()方法或是在客戶化的目標類型中接收字符串的構造方法來解析請求數據。 JAX-RS支持如下類型的注解方法參數,包括路徑變量、請求參數、HTTP頭值和cookies:
原生類型。
擁有接收單個字符串參數的構造方法的類型。
擁有一個接收單個字符串參數的名為valueOf的靜態方法的類型。
List<T>、Set<T>或是SortedSet<T>,其中的T滿足上面2個或3個要求。
Spring 3支持上面所有要求。除此之外,Spring 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格式的數據。
處理請求體中的數據
無論是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的轉換器。
准備響應
典型的響應需要准備響應代碼、設定HTTP響應頭、將數據放到響應體當中,還需要處理異常。
使用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實現 。
使用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集合也用於響應體,因此無 需再做任何配置。
狀態代碼與響應頭
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());
}
異常處理
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錯誤。這樣,每個控制器就都能處理異常了,好象來自同一個地方一樣。
總結
希望本文有助於你理解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文檔也改進頗多。
關於作者
Rossen Stoyanchev是SpringSource的高級咨詢師。在其職業生涯中,他做過貿易應用、記賬系統和電 子商務等Web應用。在 SpringSource中,Rossen專注於Web技術,包括咨詢、培訓和“Rich-Web Development With Spring”課程的內容開發,該課程旨在幫助受訓者成為認證的Spring Web應用開發者 。