jax_ws_JAX-RS和OpenAPI对Hypermedia API的支持:任重而道远

jax_ws

jax_ws

或早或晚,大多数积极使用REST(ful) Web服务和API的开发人员偶然发现了这种真正的外星事物,即HATEOAS超文本作为应用程序状态的引擎。 对HATEOAS是什么以及它与REST的关系的好奇最终将导致发现Richardson成熟度模型,该模型使RESTRESTful的行业定义神秘化。 后者是一个启发,但提出了一个问题:这些年来,我们是否一直在错误地进行REST

让我们尝试从不同的角度回答这个问题。 HATEOASREST核心架构约束之一。 从这个角度来看,答案是“是”,为了声称符合REST ,Web服务或API应该支持它。 但是,如果您四处浏览(甚至参考您过去或现在的经验),您可能会发现大多数Web服务和API只是域模型周围的CRUD包装器,而没有HATEOAS支持。 这是为什么? 可能有多个原因,但是从开发人员的工具箱角度来看, HATEOAS的支持不是那么好。

在今天的帖子中,我们将讨论有关HATEOAS的JAX-RS 2.x必须提供的内容,如何从服务器和客户端的角度使用它,以及如何增强OpenAPI v3.0.x规范以公开超媒体。作为合同的一部分。 如果您很兴奋,请让我们开始吧。

因此,我们的JAX-RS Web API将围绕管理公司及其员工而构建。 基础是Spring BootApache CXF ,其中SwaggerOpenAPI规范的实现。 AppConfig是我们需要定义的唯一配置,以启动和运行应用程序(这要归功于Spring Boot的自动配置功能)。

 @SpringBootConfiguration
 public class AppConfig {

    @Bean

    OpenApiFeature createOpenApiFeature() {

        final OpenApiFeature openApiFeature = new OpenApiFeature();

        openApiFeature.setSwaggerUiConfig( new SwaggerUiConfig().url( "/api/openapi.json" ));

        return openApiFeature;

    }
    
    @Bean

    JacksonJsonProvider jacksonJsonProvider() {

        return new JacksonJsonProvider();

    }
 }

CompanyPerson这个模型非常简单(请注意,这两个类之间没有直接关系)。

 public class Company {

    private String id;

    private String name;
 }
 public class Person {

    private String id;

    private String email;

    private String firstName;

    private String lastName;
 }

该模型通过CompanyResource公开, CompanyResource是典型的JAX-RS资源类,带有@Path注释,此外还带有OpenAPI@Tag注释。

 @Component
 @Path ( "/companies" )
 @Tag (name = "companies" )
 public class CompanyResource {

    @Autowired private CompanyService service;
 }

很好,资源类尚未定义端点,因此让我们加强一下。 我们的第一个端点将通过标识符查找公司,并以JSON格式返回其表示形式。 但是,由于我们没有包含任何与员工相关的细节,因此提示消费者(Web UI或任何其他客户端)在哪里查找真是太棒了。 有多种方法可以执行此操作,但是由于我们坚持使用JAX-RS ,因此可以使用开箱即用的Web链接( RFC-5988 )。 该代码段包含数千个单词。

 @Produces (MediaType.APPLICATION_JSON)
 @GET
 @Path ( "{id}" )
 public Response getCompanyById( @Context UriInfo uriInfo, @PathParam ( "id" ) String id) {

    return service

        .findCompanyById(id)

        .map(company -> Response

            .ok(company)

            .links(

                Link.fromUriBuilder(uriInfo

                        .getRequestUriBuilder())

                    .rel( "self" )

                    .build(),

                Link.fromUriBuilder(uriInfo

                        .getBaseUriBuilder()

                        .path(CompanyResource. class ))

                    .rel( "collection" )

                    .build(),

                Link.fromUriBuilder(uriInfo

                       .getBaseUriBuilder()

                       .path(CompanyResource. class )

                       .path(CompanyResource. class , "getStaff" ))

                    .rel( "staff" )

                    .build(id)

             )

            .build())

        .orElseThrow(() -> new NotFoundException( "The company with id '" + id + "' does not exists" ));
 }

这里发生的事情很少。 我们关心的是使用ResponseBuilder :: links方法,其中提供了三个链接。 第一个是self ,它本质上是链接上下文(定义为RFC-5988的一部分)。 第二个集合collection ,它指向CompanyResource端点,该端点返回公司列表(也包含在标准关系注册表中)。 最后,第三个是我们自己的员工关系,我们通过一个名为getStaff的方法实现的另一CompanyResource端点组装(我们将看到它不久)。 这些链接将在“链接响应”标头中提供,并指导客户端下一步去哪里。 让我们通过运行该应用程序来实际查看它。

 $ mvn clean package
 $ java -jar target/jax-rs- 2.1 -hateaos- 0.0 . 1 -SNAPSHOT.jar

并使用curl检查来自此资源端点的响应(不必要的详细信息已被滤除)。

 $ curl -v http: //localhost:8080/api/companies/1
 > GET /api/companies/ 1 HTTP/ 1.1
 > Host: localhost: 8080
 > User-Agent: curl/ 7.47 . 1
 > Accept: */*
 >
 < HTTP/ 1.1 200
 < Link: <http: //localhost:8080/api/companies/1>;rel="self"
 < Link: <http: //localhost:8080/api/companies/1/staff>;rel="staff"
 < Link: <http: //localhost:8080/api/companies>;rel="collection"
 < Content-Type: application/json
 < Transfer-Encoding: chunked
 <
 {

   "id" : "1" ,

   "name" : "HATEOAS, Inc."
 }

链接头在那里,指的是其他感兴趣的端点。 从客户的角度来看,事情看起来也很简单。 Response类提供专用的getLinks方法来包装对Link响应标头的访问,例如:

 final Client client = ClientBuilder.newClient();
 try ( final Response response = client

        .target( " http://localhost:8080/api/companies/ {id}" )

        .resolveTemplate( "id" , "1" )

        .request()

        .accept(MediaType.APPLICATION_JSON)

        .get()) {
            
    final Optional staff = response

        .getLinks()

        .stream()

        .filter(link -> Objects.equals(link.getRel(), "staff" ))

        .findFirst();
            
    staff.ifPresent(link -> {

        // follow the link here

    });
 } finally {

    client.close();
 }

到现在为止还挺好。 展望未来,由于HATEOAS本质上是Web API合同的一部分,因此让我们在桌上找出OpenAPI规范所具有的内容。 不幸的是,到目前为止尚不支持HATEOAS ,但是从好的方面来说,存在链接的概念(尽管不应它们与Web链接混淆,它们有些相似,但并不相同)。 为了说明链接在OpenAPI规范中的用法,让我们用Swagger注释装饰端点。

 @Operation (

    description = "Find Company by Id" ,

    responses = {

        @ApiResponse (

            content = @Content (schema = @Schema (implementation = Company. class )),

            links = {

                @io .swagger.v3.oas.annotations.links.Link(

                   name = "self" ,

                   operationRef = "#/paths/~1companies~1{id}/get" ,

                   description = "Find Company" ,

                   parameters = @LinkParameter (name = "id" , expression = "$response.body#/id" )

                ),

                @io .swagger.v3.oas.annotations.links.Link(

                    name = "staff" ,

                    operationRef = "#/paths/~1companies~1{id}~1staff/get" ,

                    description = "Get Company Staff" ,

                    parameters = @LinkParameter (name = "id" , expression = "$response.body#/id" )

                ),

                @io .swagger.v3.oas.annotations.links.Link(

                    name = "collection" ,

                    operationRef = "#/paths/~1companies/get" ,

                    description = "List Companies"

                )

            },

            description = "Company details" ,

            responseCode = "200"

        ),

        @ApiResponse (

            description = "Company does not exist" ,

            responseCode = "404"

        )

    }
 )
 @Produces (MediaType.APPLICATION_JSON)
 @GET
 @Path ( "{id}" )
 public Response getCompanyById( @Context UriInfo uriInfo, @PathParam ( "id" ) String id) {

  // ...
 }

如果我们运行该应用程序并浏览到浏览器中的http:// localhost:8080 / api / api-docs (这是Swagger UI的托管位置),我们将能够看到每个响应中的链接部分。

超媒体API

但是除此之外……您可以使用那里的链接做很多事情(如果您对此主题感兴趣,请注意此问题)。 吸引公司员工的资源终点看起来非常相似。

 @Operation (

    description = "Get Company Staff" ,

    responses = {

        @ApiResponse (

            content = @Content (array = @ArraySchema (schema = @Schema (implementation = Person. class ))),

            links = {

                @io .swagger.v3.oas.annotations.links.Link(

                    name = "self" ,

                    operationRef = "#/paths/~1companies~1{id}~1staff/get" ,

                    description = "Staff" ,

                    parameters = @LinkParameter (name = "id" , expression = "$response.body#/id" )

                ),

                @io .swagger.v3.oas.annotations.links.Link(

                    name = "company" ,

                    operationRef = "#/paths/~1companies~1{id}/get" ,

                    description = "Company" ,

                    parameters = @LinkParameter (name = "id" , expression = "$response.body#/id" )

                )

            },

            description = "The Staff of the Company" ,

            responseCode = "200"

        ),

        @ApiResponse (

            description = "Company does not exist" ,

            responseCode = "404"

        )

    }
 )
 @Produces (MediaType.APPLICATION_JSON)
 @GET
 @Path ( "{id}/staff" )
 public Response getStaff( @Context UriInfo uriInfo, @PathParam ( "id" ) String id) {

    return service

        .findCompanyById(id)

        .map(c -> service.getStaff(c))

        .map(staff -> Response

            .ok(staff)

            .links(

                Link.fromUriBuilder(uriInfo

                        .getRequestUriBuilder())

                    .rel( "self" )

                    .build(),

                Link.fromUriBuilder(uriInfo

                        .getBaseUriBuilder()

                        .path(CompanyResource. class )

                        .path(id))

                    .rel( "company" )

                    .build()

             )

            .build())

        .orElseThrow(() -> new NotFoundException( "The company with id '" + id + "' does not exists" ));
 }

如您所料,除了指向self的链接之外,它还包括指向公司的链接。 当我们使用curl尝试时,预期的响应头将返回。

 $ curl -v http: //localhost:8080/api/companies/1/staff
 > GET /api/companies/ 1 /staff HTTP/ 1.1
 > Host: localhost: 8080
 > User-Agent: curl/ 7.47 . 1
 > Accept: */*
 >
 < HTTP/ 1.1 200
 < Link: <http: //localhost:8080/api/companies/1/staff>;rel="self"
 < Link: <http: //localhost:8080/api/companies/1>;rel="company"
 < Content-Type: application/json
 < Transfer-Encoding: chunked
 <
 [

    {

        "id" : "1" ,

        "email" : "john@smith.com" ,

        "firstName" : "John" ,

        "lastName" : "Smith"

    },

    {

        "id" : "2" ,

        "email" : "bob@smith.com" ,

        "firstName" : "Bob" ,

        "lastName" : "Smith"

    }
 ]

那么我们可以得出什么样的结论呢? HATEOAS实际上通过动态地驱动对话来统一Web API提供者和使用者之间的交互模型。 这是非常强大的功能,但是其中的大多数框架和工具都没有对HATEOAS的相当基本的支持(例如Web Linking ),或者根本没有。

在很多情况下,使用Web链接就足够了(到目前为止,我们已经看到了示例,例如分页,导航等),但是假设创建,编辑或修补现有资源又如何呢? 如何用超媒体丰富集合中返回的各个元素(在RFC-6537中进行描述)? HATEOAS是否值得所有这些努力?

与往常一样,答案是“取决于”,也许我们应该超越JAX-RS ? 在下一篇文章中(s_,我们将继续解决问题。

完整的源代码可在Github上找到

翻译自: https://www.javacodegeeks.com/2019/02/hypermedia-apis-support-jax-rs-openapi.html

jax_ws

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值