zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    BZOJ2821 作诗(Poetize) 【分块】
    BZOJ2724 蒲公英 【分块】
    Codeforces 17E Palisection 【Manacher】
    BZOJ2565 最长双回文串 【Manacher】
    Codeforces 25E Test 【Hash】
    CODEVS3013 单词背诵 【Hash】【MAP】
    HDU2825 Wireless Password 【AC自动机】【状压DP】
    HDU2896 病毒侵袭 【AC自动机】
    HDU3065 病毒侵袭持续中【AC自动机】
    HDU2222 Keywords Search 【AC自动机】
  • 原文地址:https://www.cnblogs.com/oopsguy/p/7503589.html
Copyright © 2011-2022 走看看