原文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. 初学 Python(十三)——匿名函数

    初学 Python(十三)--匿名函数 初学 Python,主要整理一些学习到的知识点,这次是匿名函数. # -*- coding:utf-8 -*- #关键字lambda定义的函数都是匿名函数 #做 ...

  2. 利用C#进行Socket通信编程之二:一个实例

    本文转载自: http://blog.csdn.net/huangxinfeng/article/details/4967629/

  3. 使用我的编译器,下面的代码 int i=7; printf("%d\n", i++ * i++); 返回 49?不管按什么顺序计算, 难道不该打印出56吗?

    尽管后缀自加和后缀自减操作符 ++ 和 -- 在输出其旧值之后才会执行运算, 但这里的"之后"常常被误解.没有任何保证确保自增或自减会在输出变量原值之 后和对表达式的其它部分进行计 ...

  4. Mongodb启动&关闭

    mac 下mongo的启动和关闭以及启动问题解决 mongo的安装在这:http://www.cnblogs.com/leinov/p/6855784.html Mac os mongodb数据安装路 ...

  5. 关于Web.config的debug和release.config文件

    使用Web.Config Transformation配置灵活的配置文件 发布Asp.net程序的时候,开发环境和发布环境的Web.Config往往不同,比如connectionstring等.如果常 ...

  6. ASP.NET Core - Razor 页面简介

    简介 随着ASP.NET Core 2 即将来临,最热门的新事物是Razor页面.在之前的一篇文章中,我们简要介绍了ASP.NET Core Razor 页面. Razor页面是ASP.NET Cor ...

  7. RabbitMQ安装以及java使用(二)

    上一篇记录了rabbitmq的安装,这一篇记录一下rabbitmq的java客户端的简单使用,当然在项目中我们有更为复杂的应用场景,这里只有最简单的点对点生产者与消费者模式. 1.建立工程 首先建立一 ...

  8. 实现Echarts折线图的虚实转换

    需求:医院的体温单,在统计体温时,对于正常情况下统计的体温数据,需要显示实线:对于进行物理降温后统计的体温数据,需要显示虚线. 现有的体温单是运用 Echarts 折线图,统一用实线显示.因此在这基础 ...

  9. noip普及组2007 纪念品分组

    纪念品分组 描述 元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作.为使得参加晚会的同学所获得 的纪念品价值相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品, 并且每组 ...

  10. cycript

    cycript是大神saurik开发的一个非常强大的工具,可以让开发者在命令行下和应用交互,在运行时查看和修改应用.它确实可以帮助你破解一些应用,但我觉得这个工具主要还是用来学习其他应用的设计(主要是 ...