原文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 和其他属性:planetOfOriginspeciestype

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 中的组件
  • @BeanDefaultStormtrooperDao 实例绑定到 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 有一个示例,可在 JerseyRestEasyApache 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 配置类和我们的资源类。

我们的 SpringBootAppStormtrooperResource 类与之前的版本相同,唯一的区别就是 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

REST:JAX-RS 与 Spring的更多相关文章

  1. Spring的远程调用

    Spring远程支持是由普通(Spring)POJO实现的,这使得开发具有远程访问功能的服务变得相当容易 四种远程调用技术: ◆ 远程方法调用(RMI) ◆ Caucho的Hessian和Burlap ...

  2. Spring中HttpInvoker远程方法调用总结

    Spring为各种远程訪问技术的集成提供了工具类.Spring远程支持是由普通(Spring)POJO实现的,这使得开发具有远程訪问功能的服务变得相当easy. 眼下,Spring支持四种远程技术: ...

  3. 使用Spring进行远程访问与Web服务

    1.1. 简介   Spring为各种远程访问技术的集成提供了整合类.Spring使得开发具有远程访问功能的服务变得相当容易,而这些远程访问服务由普通Spring POJO实现.目前,Spring支持 ...

  4. 调用链系列二、Zipkin 和 Brave 实现(springmvc、RestTemplate)服务调用跟踪

    Brave介绍 1.Brave简介 Brave 是用来装备 Java 程序的类库,提供了面向标准Servlet.Spring MVC.Http Client.JAX RS.Jersey.Resteas ...

  5. Zipkin和Brave实现http服务调用的跟踪

    使用Zipkin和Brave实现http服务调用的跟踪,Brave 是用来装备Java程序的类库,提供了面向标准Servlet.Spring MVC.Http Client.JAX RS.Jersey ...

  6. 原理分析dubbo分布式应用中使用zipkin做链路追踪

    zipkin是什么 Zipkin是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper的论文设计而来,由 Twitter 公司开 ...

  7. 使用 Zipkin 和 Brave 实现分布式系统追踪(基础篇)

    一.Zipkin 1.1.简介 Zipkin 是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper 的论文设计而来,由 Twi ...

  8. java各种框架的比较,分析

    Spring 框架 优点 1.提供了一种管理对象的方法,可以把中间层的对象有效地组织起来 2.采用了分层结构,可以增量引入到项目中. 3.代码测试较容易 4.非侵入性,应用程序对Spring API的 ...

  9. apache基金会开源项目简介

    apache基金会开源项目简介   项目名称 描述 HTTP Server 互联网上首屈一指的HTTP服务器 Abdera Apache  Abdera项目的目标是建立一个功能完备,高效能的IETF ...

  10. Zipkin — 微服务链路跟踪.

    一.Zipkin 介绍 Zipkin 是什么?  Zipkin的官方介绍:https://zipkin.apache.org/  Zipkin是一款开源的分布式实时数据追踪系统(Distributed ...

随机推荐

  1. ue4粒子实现流血效果

    ---恢复内容开始--- 动作/射击游戏中,击中角色时常常伴随着血花效果,增强打击感的同时,也方便了玩家对命中与否的判断. 血液效果分两块,首先是受伤部位在受击瞬间产生血雾粒子,然后在身体.地面.墙面 ...

  2. python中全局变量和局部变量的一个小坑

    python 中全局变量和局部变量在使用过程中的一个容易出错的地方 什么是全局变量 python中,在函数外部声明的变量可以叫做全局变量. x = 10 def fn1(): pass fn1() 什 ...

  3. webpack模块解析

    前面的话 在web存在多种支持JavaScript模块化的工具(如requirejs和r.js),这些工具各有优势和限制.webpack基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文 ...

  4. python-希尔排序

    希尔排序 希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名. 希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本.希尔排序是非稳定排序算法 ...

  5. node 内存管理相关

    为什么在node中要担心node内存管理 使用JavaScript进行前端开发时几乎完全不需要关心内存管理问题,对于前端编程来说,V8限制的内存几乎不会出现用完的情况,v8在node中有着内存的限制( ...

  6. JS获取字符串长度(区分中英文)

    JS获取字符串长度(区分中英文) 中文算2个字,英文一个. function getStrLength(str) {      var cArr = str.match(/[^\x00-\xff]/i ...

  7. [STL] day 1~2 Problem Set

    Q#1 #include <cmath> #include <cstdio> #include <vector> #include <iostream> ...

  8. web添加第三方应用,前端解决跨域问题的8种方案

    应用场景 web应用通过QQ登录授权实现第三方登录. 操作步骤 1. 注册成为QQ互联平台开发者,http://connect.qq.com/ 2. 准备一个可访问的域名,如dev.foo.com 3 ...

  9. 【NO.7】HTTP请求-参数化

    http://123.456.7.89:8080/article/relation/channel/0038/keyword/movie/start/0/size/20/ 简单说一下这个URL的意思也 ...

  10. jQuery.extend 使用函数

    介绍 jQuery.extend([deep], target, object1, [objectN])用一个或多个其他对象来扩展一个对象,返回被扩展的对象.如果不指定target,则给jQuery对 ...