Oopsguy
http://oopsguy.com

REST:JAX-RS 与 Spring

原文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

posted @ 2017-09-11 10:03  oopsguy  阅读(2582)  评论(2编辑  收藏  举报