1. 程式人生 > >【乾貨】Spring MVC與JAX-RS比較與分析

【乾貨】Spring MVC與JAX-RS比較與分析

過去幾年,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進行開發,那可能想知道(或者有人曾問過你)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文件也改進頗多。