或許您正在使用 REST 端點(endpoint)來擺脫 Web 服務和客戶端。如果您是一名 Java 開發人員,您可能已經嘗試過 JAX-RS、Spring REST 或者兩者。但哪一個好用呢?在這篇文章中,我將介紹兩者之間的差異,使用大體相同的代碼進行對比。在之後的博文中,我將向您展示如何輕鬆地... ...
原文:https://developer.okta.com/blog/2017/08/09/jax-rs-vs-spring-rest-endpoints
作者:Brian Demers
譯者:http://oopsguy.com
或許您正在使用 REST 端點(endpoint)來擺脫 Web 服務和客戶端。如果您是一名 Java 開發人員,您可能已經嘗試過 JAX-RS、Spring REST 或者兩者。但哪一個好用呢?在這篇文章中,我將介紹兩者之間的差異,使用大體相同的代碼進行對比。在之後的博文中,我將向您展示如何輕鬆地使用 Apache Shiro 和 Okta 來保護這些 REST 端點。
模型與 DAO
為了突出重點,我不再介紹本次示例所用的 Maven 依賴。您可以在 Github 上瀏覽完整的源碼,pom 文件應該描述得很清楚了:一個用於 JAX-RS,其他用於 Spring。
首先,我們需要把某些通用部分提取出來。所有示例中都使用到了一個簡單的模型和 DAO(Data Access Object,數據訪問對象)來註冊和管理 Stormtrooper
對象。
public class Stormtrooper {
private String id;
private String planetOfOrigin;
private String species;
private String type;
public Stormtrooper() {
// empty to allow for bean access
}
public Stormtrooper(String id, String planetOfOrigin, String species, String type) {
this.id = id;
this.planetOfOrigin = planetOfOrigin;
this.species = species;
this.type = type;
}
...
// bean accessor methods
Stormtrooper
對象包含id
和其他屬性:planetOfOrigin
、species
和 type
。
DAO 介面也很簡單,使用基本的 CRUD 方法和一個額外的 list
方法:
public interface StormtrooperDao {
Stormtrooper getStormtrooper(String id);
Stormtrooper addStormtrooper(Stormtrooper stormtrooper);
Stormtrooper updateStormtrooper(String id, Stormtrooper stormtrooper);
boolean deleteStormtrooper(String id);
Collection<Stormtrooper> listStormtroopers();
}
StormtrooperDao
的具體實現對於這些示例來說並不重要,如果您感興趣,可以查看 DefaultStormtrooperDao
的代碼,該代碼生成了 50 個隨機的 Stormtrooper。
嘗試 Spring
我們提取了通用部分,現在可以開始 Spring 示例了。這是一個再簡單不過的 Spring Boot 應用程式:
@SpringBootApplication
public class SpringBootApp {
@Bean
protected StormtrooperDao stormtrooperDao() {
return new DefaultStormtrooperDao();
}
public static void main(String[] args) {
SpringApplication.run(SpringBootApp.class, args);
}
}
有幾點要指出的是:
@SpringBootApplication
註解設置啟用 Spring 自動配置和掃描 classpath 中的組件@Bean
將DefaultStormtrooperDao
實例綁定到StormtrooperDao
介面main
方法使用SpringApplication.run()
輔助方法來引導應用程式
Spring 控制器
接下來,我們要實現 REST 端點,也可以說是 Spring 中的一個 Controller。我們使用該類來將 DAO 映射到傳入的 HTTP 請求。
@RestController
@RequestMapping("/troopers")
public class StormtrooperController {
private final StormtrooperDao trooperDao;
@Autowired
public StormtrooperController(StormtrooperDao trooperDao) {
this.trooperDao = trooperDao;
}
@GetMapping
public Collection<Stormtrooper> listTroopers() {
return trooperDao.listStormtroopers();
}
@GetMapping("/{id}")
public Stormtrooper getTrooper(@PathVariable("id") String id) throws NotFoundException {
Stormtrooper stormtrooper = trooperDao.getStormtrooper(id);
if (stormtrooper == null) {
throw new NotFoundException();
}
return stormtrooper;
}
@PostMapping
public Stormtrooper createTrooper(@RequestBody Stormtrooper trooper) {
return trooperDao.addStormtrooper(trooper);
}
@PostMapping("/{id}")
public Stormtrooper updateTrooper(@PathVariable("id") String id,
@RequestBody Stormtrooper updatedTrooper) throws NotFoundException {
return trooperDao.updateStormtrooper(id, updatedTrooper);
}
@DeleteMapping("/{id}")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void deleteTrooper(@PathVariable("id") String id) {
trooperDao.deleteStormtrooper(id);
}
}
讓我們來分解以下代碼:
@Controller
@RequestMapping("/troopers")
public class StormtroooperController {
@RestController
是 @Controller
和 @ResponseBody
的快捷註解,它將此類標記為在 classpath 掃描期間要發現的 Web 組件。類級別的 @RequestMapping
註解定義了用於此類中任何 RequestMapping
註解的基本路徑映射。示例中,此類中的所有端點將以 URL /troopers
為開頭。
@PostMapping("/{id}")
public @ResponseBody Stormtrooper updateTrooper(@PathVariable("id") String id,
@RequestBody Stormtrooper updatedTrooper) throws NotFoundException {
return trooperDao.updateStormtrooper(id, updatedTrooper);
}
PostMapping
是 @RequestMapping
註解的 POST 別名,它有許多選項,此示例只使用一小部分:
- 與
@PathVariable("id")
結合使用的path = "/{id}"
將 URL 路徑中的{id}
部分映射到給定的方法參數 - 示例URL:/troopers/FN-2187
value = HttpStatus.NO_CONTENT
設置需要返回的HTTP
響應代碼,即204
狀態碼
使用了 @RequestBody
註解的方法參數將在被傳遞給該方法之前從 HTTP 請求反序列化。使用 @ResponseBody
註解(或簡單地使用 @RestController
),返回值直接被序列化為 HTTP 響應,同時將繞過所有 MVC 模板。
在此代碼塊中,updateTrooper()
方法接收了對 /trooper/{id}
的 HTTP POST 請求,此請求包含了一個序列化的 Stormtrooper
(JSON)。如果請求路徑為 /troopers/FN-2187
,路徑的 id
部分將被分配給方法的 id
參數。之後將更新後的 Stormtrooper
對象返回並序列化為 HTTP 響應。
在上面的例子中,我們簡單地使用 POST 應用於創建和更新方法。為了讓這個例子更加美觀簡潔,實際上 DAO 實現並不做部分更新,所以應該是一個 PUT。看看這篇博文,瞭解更多關於什麼時候使用 PUT 和 POST。
運行 Spring 示例
要運行此示例,請下載源碼,切換到 spring-boot
目錄下,使用 mvn spring-boot:run
啟動應用程式,並向伺服器發出請求。
要得到所有 Stormtrooper 的列表,只需要向 /troopers
發出請求。
$ curl http://localhost:8080/troopers
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Tue, 08 Nov 2016 20:33:36 GMT
Transfer-Encoding: chunked
X-Application-Context: application
[
{
"id": "FN-2187",
"planetOfOrigin": "Unknown",
"species": "Human",
"type": "Basic"
},
{
"id": "FN-0984",
"planetOfOrigin": "Coruscant",
"species": "Human",
"type": "Aquatic"
},
{
"id": "FN-1253",
"planetOfOrigin": "Tatooine",
"species": "Unidentified",
"type": "Sand"
},
...
]
要獲取單個 Stormtrooper,可以利用它的 ID:
$ curl http://localhost:8080/troopers/FN-2187
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Tue, 08 Nov 2016 20:38:53 GMT
Transfer-Encoding: chunked
X-Application-Context: application
{
"id": "FN-2187",
"planetOfOrigin": "Unknown",
"species": "Human",
"type": "Basic"
}
相當簡單吧?現在您可以使用 Ctrl-C
來停止伺服器,並轉到下一個示例。
JAX-RS
我們在 JAX-RS 示例中使用相同的模型和 DAO,我們所需要做的只有更改 StormtroooperController
類的註解。
由於 JAX-RS 是一個 API 規範,您需要選擇一個實現,在本示例中,我們將使用 Jersey 作為實現。雖然可以創建一個沒有直接依賴於特定 JAX-RS 實現的 JAX-RS 應用程式,但這將使得示例更加啰嗦。
我選擇 Jersey 有幾個原因,主要是因為我可以不用繞圈子就可以輕鬆地獲得簡單的依賴註入,畢竟我們是把它在和 Spring 做對比。Apache Shiro 有一個示例,可在 Jersey、RestEasy 和 Apache CXF 上運行相同的代碼,如果你感興趣不妨看一看。
此示例與 Spring Boot 不同之處在於,它打包成 WAR,而 Spring Boot 是單個 JAR。此示例也可以打包進可執行的 jar 中,但此內容不在本文範圍之內。
在 JAX-RS 中與 SpringBootApplication
相當的是一個 Application
類。Jersey 的 Application
子類 ResourceConfig
添加了一些便捷的實用方法。以下代碼配置 classpath 掃描以檢測我們的各個資源類,並將 DefaultStormtrooperDao
實例綁定到 StromtrooperDao
介面。
@ApplicationPath("/")
public class JaxrsApp extends ResourceConfig {
public JaxrsApp() {
// scan the resources package for our resources
packages(getClass().getPackage().getName() + ".resources");
// use @Inject to bind the StormtrooperDao
register(new AbstractBinder() {
@Override
protected void configure() {
bind(stormtrooperDao()).to(StormtrooperDao.class);
}
});
}
private StormtrooperDao stormtrooperDao() {
return new DefaultStormtrooperDao();
}
}
另外要指出的是,在上面的類中,@ApplicationPath
註解將這個類標記為一個 JAX-RS
應用程式並綁定到一個特定的 url 路徑,這匹配了上面的 Spring 例子,我們只使用了根路徑:/
。資源包中檢測到的每個資源都將被追加到該基本路徑。
JAX-RS 資源實現看起來非常類似於上述的 Spring 版本(重命名為 StormtroooperResource
,以符合命名約定):
@Path("/troopers")
@Produces("application/json")
public class StormtroooperResource {
@Inject
private StormtrooperDao trooperDao;
@Path("/{id}")
@GET
public Stormtrooper getTrooper(@PathParam("id") String id) throws NotFoundException {
Stormtrooper stormtrooper = trooperDao.getStormtrooper(id);
if (stormtrooper == null) {
throw new NotFoundException();
}
return stormtrooper;
}
@POST
public Stormtrooper createTrooper(Stormtrooper trooper) {
return trooperDao.addStormtrooper(trooper);
}
@Path("/{id}")
@POST
public Stormtrooper updateTrooper(@PathParam("id") String id,
Stormtrooper updatedTrooper) throws NotFoundException {
return trooperDao.updateStormtrooper(id, updatedTrooper);
}
@Path("/{id}")
@DELETE
public void deleteTrooper(@PathParam("id") String id) {
trooperDao.deleteStormtrooper(id);
}
@GET
public Collection<Stormtrooper> listTroopers() {
return trooperDao.listStormtroopers();
}
}
我們先來分解以下片段:
@Path("/troopers")
@Produces("application/json")
public class StormtroooperResource {
類似於上面的 Spring 示例,類級別上的 @Path
表示此類中的每個註解方法都將位於 /troopers
基本路徑下。@Produces
註解定義了預設響應內容類型(除非被其他方法的註解所覆蓋)。
與 Spring 示例不同,其中 @RequestMapping
註解定義了請求的路徑、方法和其他屬性,在 JAX-RS 資源中,每個屬性都使用單獨的註解。與上述類似,如果我們分解了 updateTrooper()
方法:
@Path("/{id}")
@POST
public Stormtrooper updateTrooper(@PathParam("id") String id,
Stormtrooper updatedTrooper) throws NotFoundException {
return trooperDao.updateStormtrooper(id, updatedTrooper);
}
我們看到 @Path("/{id}")
以及 @PathParam("id")
允許將路徑的 id
部分轉換為方法參數。與 Spring 示例不同的是,Stromtrooper
參數和返回值不需要額外的註解,由於此類上的 @Produces("application/json")
註解,它們將自動序列化/反序列化為 JSON。
運行 JAX-RS 示例
進入 Jersey 目錄,使用 maven 命令:mvn jetty:run
運行此示例。
發出與上述相同的兩個請求,我們可以發出 GET
請求列出所有 trooper:
$ curl http://localhost:8080/troopers
HTTP/1.1 200 OK
Content-Length: 3944
Content-Type: application/json
Date: Tue, 08 Nov 2016 21:57:55 GMT
Server: Jetty(9.3.12.v20160915)
[
{
"id": "FN-2187",
"planetOfOrigin": "Unknown",
"species": "Human",
"type": "Basic"
},
{
"id": "FN-0064",
"planetOfOrigin": "Naboo",
"species": "Nikto",
"type": "Sand"
},
{
"id": "FN-0069",
"planetOfOrigin": "Hoth",
"species": "Twi'lek",
"type": "Basic"
},
{
"id": "FN-0169",
"planetOfOrigin": "Felucia",
"species": "Kel Dor",
"type": "Jump"
},
...
或者 GET
一個特定的資源:
$ curl http://localhost:8080/troopers/FN-2187
HTTP/1.1 200 OK
Content-Length: 81
Content-Type: application/json
Date: Tue, 08 Nov 2016 22:00:02 GMT
Server: Jetty(9.3.12.v20160915)
{
"id": "FN-2187",
"planetOfOrigin": "Unknown",
"species": "Human",
"type": "Basic"
}
現在我們已經看到了基本相同的代碼在 Spring 和 JAX-RS 應用程式中運行,只需更改註解即可。我更喜歡 JAX-RS 的註解,他們更簡潔。既然如此,為什麼要在兩者選擇呢?Jersey 和 RestEasy 都支持 Spring(以及 Guice 和 CDI/Weld)。讓我們來創建一個結合了這兩者的第三個例子。
JAX-RS 與 Spring 整合
針對此示例,我們需要三個類:Spring Boot 應用類、Jersey 配置類和我們的資源類。
我們的 SpringBootApp
和 StormtrooperResource
類與之前的版本相同,唯一的區別就是 Jersey 配置類:
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
// scan the resources package for our resources
packages(getClass().getPackage().getName() + ".resources");
}
}
該類與之前的示例有點類似。首先,您可能註意到了用於標記此類由 Spring 管理的 `@Configuration 註解。剩下的就是指示 Jersey 再次掃描資源包,其餘的都是您的處理邏輯。
進入 spring-jaxrs
目錄中,使用 mvn spring-boot:run
命令啟動此示例。
Spring 與 JAX-RS 對照表
為了幫助您在 Spring 和 JAX-RS 的之間作出區分,這裡給出了一份對照表。儘管不是很詳盡,但它包含最常見的註解。
Spring Annotation | JAX-RS Annotation |
---|---|
@RequestMapping(path = "/troopers") | @Path("/troopers") |
@PostMapping | @POST |
@PutMapping | @PUT |
@GetMapping | @GET |
@DeleteMapping | @DELETE |
@ResponseBody | N/A |
@RequestBody | N/A |
@PathVariable("id") | @PathParam("id") |
@RequestParam("xyz") | @QueryParam("xyz") |
@RequestParam(value="xyz") | @FormParam("xyz") |
@RequestMapping(produces = {"application/json"}) | @Produces("application/json") |
@RequestMapping(consumes = {"application/json"}) | @Consumes("application/json") |
何時在 Spring 上使用 JAX-RS?
如果你已經是一個 Spring 用戶,就使用 Spring 吧。如果你正在創建一個對象 JSON/XML REST 層,那麼您選擇的 DI 框架(如 Spring、Guice 等)支持 JAX-RS 資源可能是一個不錯的選擇。伺服器端渲染頁面並不是 JAX-RS 規範的一部分(雖然它是擴展支持的)。我曾在 Jersey 中使用了 Thymeleaf 視圖,但我認為這是 Spring MVC 該做的。
目前為止,我們還沒有把 Spring Boot 應用程式與 WAR 打包的應用程式進行詳細地對比。Dropwizard(使用嵌入式 Jetty 容器和 Jersey)可能是與 Spring Boot 應用程式最接近的。希望這篇文章能給你帶來一些靈感,您可以做自己的對比。如果您有任何問題,歡迎 Twitter @briandemers!
示例代碼
https://github.com/oktadeveloper/jaxrs-spring-blog-example