restful和rest_HATEOAS的RESTful服务:JVM上的REST API和超媒体

restful和rest

1.简介

到目前为止,我们已经花了很多时间谈论了相当数量的关于角色的的超媒体HATEOASREST风格的 Web服务和API,扫视不同规格和可用性方面。 听起来好像支持超媒体HATEOAS并不难,只需选择您的收藏夹就可以了! 您可能会猜到,现实是“好,取决于情况”,在本教程的这一部分中,我们将理解“为什么”。 困难的学习方法是从头开始设计和实现它们。 这正是我们将要忙的事情,主要在Java中将RESTful Web API和客户端引入JVM平台。

我们将为其设计RESTful Web API的应用程序是我们之前涉及的案例研究的扩展版本。 从业务角度来看,我们将实施一个可以让客户租车的租车平台。 我们的目标是根据REST架构风格的原则和约束来构建该平台。

2.从CRUD到工作流程

超媒体作为驱动力的存在极大地改变了设计过程。 不幸的事实是,目前大多数基于HTTP的Web服务和API基本上都是CRUD代理,用于其后面的数据存储。 仅仅公开数据模型并不能帮助客户理解他们可以做什么,他们注定要在许多地方复制服务器的业务逻辑(至少是业务逻辑的大部分)。 这不是经过深思熟虑的REST应用程序应该做的。

REST API应该花费几乎所有的描述性精力来定义用于表示资源和驱动应用程序状态的媒体类型,或者为现有标准媒体类型定义扩展关系名称和/或启用超文本的标记。 花费所有精力描述应该在媒体类型的处理规则范围内(并且在大多数情况下已由现有媒体类型定义)完全定义对感兴趣的URI使用哪种方法。

https://roy.gbiv.com/untangled/2008/rest-apis-must-be-超文本驱动

代替CRUD ,我们应该将RESTful Web API视为工作流。 通过就适当的媒体类型达成共识,服务器和客户端可以就如何解释它们达成共识。 链接,关系和操作(支付能力)的存在会引导客户采取可能的后续步骤。 换句话说,服务定义并共享供客户遵循的工作流。

3.在服务器上

在深入研究库和框架之前,最好先了解我们正在尝试作为应用程序一部分构建的RESTful Web API的工作流程。 下图是这样做的尝试。

JVM上的REST API-租车API工作流程
租车API工作流程

诚然,工作流远非详尽无遗,但足以说明现实世界中RESTful Web API的复杂性,挑战和好处。

最后但并非最不重要的一点是,我们尚未决定要使用哪种超媒体规范HAL的简单性,轻量级结构和广泛采用(以及HAL-FORMS )使其成为大多数情况下的不错选择,这就是我们将用于RESTful Web API的方式。

制定战略决策后,就该讨论技术细节了。 首先,我们正在寻找对我们有帮助的库或框架。

JAX-RS

Jakarta RESTful Web服务规范(更好地称为JAX-RS 2.1JSR-370 ))是在JVM平台上实现RESTful Web服务和API的最受欢迎选择之一。

JAX-RSRESTful Web服务的Java API JAX-RS 是一种Java编程语言API规范,它提供了根据代表性状态转移(REST)架构模式创建Web服务的支持。

https://projects.eclipse.org/projects/ee4j.jaxrs

尽管它包括全面的服务器端和客户端支持,但除了引入非常基本的Link表示之外,它几乎没有解决任何超媒体功能。

一些JAX-RS参考实现(例如Jersey )进行了努力,并捆绑了自己的专有扩展以促进对超媒体的支持,但是正如您所期望的那样,这些都不是规范的一部分。 这当然是有帮助的,但仍然需要开发人员完成大量工作。

Quarkus,Micronaut,Helidon……

微服务架构和云计算的加速转变导致了新一代的云原生框架的泛滥。 对于JVM平台尤其如此,在JVM平台上,著名的领导者正受到QuarkusMicronautHelidon等人的挑战。

实际上,对于大多数媒体 而言超媒体HATEOAS并不是首要任务。 Micronaut是异常值的一个示例,该异常值至少包含基本的超媒体元素 ,但总的来说,您是一个人。

Crnk

如果您碰巧选择了JSON:API规范来支持RESTful Web服务和API,那么Crnk框架当然值得一看。

CrnkJSON API规范和Java中建议的实现,以促进构建RESTful应用程序。 它提供了许多可以使应用程序受益的约定和构造块。 这包括诸如排序,过滤,分页,请求复杂对象图,稀疏字段集,将链接附加到数据或自动执行多项操作等功能。 与框架和库(例如Spring,CDI,JPA,Bean验证,Dropwizard,Servlet API,Zipkin等)的进一步集成可确保JSON API与Jav​​a生态系统完美地结合在一起。

https://github.com/crnk-project/crnk-framework

Crnk采用以资源为中心的API建模方法,基本上可以实现非常干净且可维护的实现。 引用文档, 资源,关系存储库Crnk的核心构建块。 下面的代码段很好地说明了这些概念。

@JsonApiResource(type = "customer", resourcePath = "customers")
public class Customer {
    @JsonApiId private String id;
    @NotNull @NotBlank private String firstName;
    @NotNull @NotBlank private String lastName;
    @JsonApiRelation(mappedBy = "customer")
    private Collection<Reservation> reservations;

    // Getters and setters here
    ...
}

@JsonApiResource(type = "reservation", resourcePath = "reservations")
public class Reservation {
    @JsonApiId private String id;
    private String vehicle;
    @NotNull @FutureOrPresent private LocalDate from;
    @NotNull @FutureOrPresent private LocalDate to;
    @JsonApiRelation private Customer customer;

    // Getters and setters here
    ...
}

@Repository
public class CustomerRepository implements ResourceRepository<Customer, String> {
    // Implementation of the repository methods here
    ...
}

@Repository
public class ReservationRepository implements ResourceRepository<Reservation, String>  {
    // Implementation of the repository methods here
    ...
}

基本上就是这样,根据集成( Vert.XJAX-RSSpring Boot ), Crnk框架将为您完成其余工作。 不幸的是, Crnk不支持ALPSJSON Hyper-Schema (至少是开箱即用的)。

但是由于我们决定使用HAL而不是JSON:API ,所以我们必须继续搜索。

Spring帽子

构成Spring产品组合的项目数量确实令人印象深刻。 我们特别感兴趣的是Spring HATEOAS ,它是一个库,用于支持实现超媒体驱动的RESTful Web服务和API的表示形式。 它实现了HALHAL-FORMSCollection + JSONUBER规范,并且加分了一点,它带有ALPS支持,非常适合我们完成已设定的目标。

如人们所料, Spring HATEOAS自然地与典型的Spring Boot Web应用程序集成,包括传统的Spring MVC和响应式Spring WebFlux堆栈。 @EnableHypermediaSupport批注以及Spring Boot自动配置功能会根据您选择的一个(或多个)规范来激活超媒体支持。

@SpringBootConfiguration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class ReservationServiceConfig {
    @Bean
    HalFormsConfiguration halFormsConfiguration() {
        final HalFormsConfiguration configuration = new HalFormsConfiguration();
        configuration.registerPattern(LocalDate.class, "yyyy-MM-dd");
        return configuration;
    }
}

遵循非正式的命名约定, RootController我们的RESTful Web API提供RootController

@RestController
public class RootController {
    @GetMapping("/")
    public ResponseEntity<RepresentationModel<?>> root() {
        final RepresentationModel<?> model = new RepresentationModel<>();
        model.add(linkTo(methodOn(RootController.class).root()).withSelfRel());
        model.add(templated(linkTo(methodOn(ReservationController.class).findAll(null)), "reservations")
            .withProfile(linkTo(methodOn(RootController.class).reservations()).withSelfRel().getHref()));
        model.add(linkTo(methodOn(CustomerController.class).findAll())
            .withRel("customers")
            .withProfile(linkTo(methodOn(RootController.class).customers()).withSelfRel().getHref()));
        return ResponseEntity.ok(model);
    }
}

控制器返回的HAL文档提示了下一个可用的导航方向。

{                                                                         
  "_links": {                                                            
    "self": {                                                            
      "href": "https://rentals.jcg.com"                                   
    },                                                                    
    "reservations": {                                                    
      "href": "https://rentals.jcg.com/reservations{?page,size,sort}",     
      "profile": "https://rentals.jcg.com/alps/reservations",              
      "templated" : true                                                  
    },                                                                    
    "customers": {                                                       
      "href": "https://rentals.jcg.com/customers",                         
      "profile": "https://rentals.jcg.com/alps/customers"                  
    }                                                                     
  }                                                                       
}

有些事情可能引起您的注意。 第一个是reservations关系的链接,该链接作为模板返回。 第二个是每个链接关系的profile属性的存在,指向各自的ALPS配置文件。 下面的代码段说明了用于构建reservations收集资源的ALPS配置文件的Spring HATEOAS API。

@GetMapping(value = "/alps/reservations", produces = MediaTypes.ALPS_JSON_VALUE)
public ResponseEntity<Alps> reservations() {
   return ResponseEntity.ok(Alps
       .alps()
       .doc(doc()
           .href("https://rentals.jcg.com/documentation.html")
           .build())
       .descriptor(List.of(
           descriptor()
               .id("reservations")
               .type(Type.SEMANTIC)
               .rt("#reservation")
               .descriptor(Arrays.asList(
                   descriptor()
                       .id("book")
                       .name("reservations")
                       .type(Type.UNSAFE)
                       .rt("#reservation")
                       .build(),
                   descriptor()
                       .id("list")
                       .name("reservations")
                       .type(Type.SAFE)
                       .rt("#reservation")
                       .build()
                   ))
               .build(),
           descriptor()
               .id("reservation")
               .type(Type.SEMANTIC)
               .descriptor(Stream
                   .concat(
                       PropertyUtils
                           .getExposedProperties(Reservation.class)
                           .stream()
                           .map(property -> descriptor()
                               .id(property.getName())
                               .href(href(property)) 
                               .type(Type.SEMANTIC)
                               .build()),
                       Stream.of(
                            descriptor()
                               .id("customer")
                               .type(Type.SAFE)
                               .rt("#customer")
                               .build(),
                           descriptor()
                               .id("update")
                               .name("reservation")
                               .type(Type.IDEMPOTENT)
                               .rt("#reservation")
                               .build(),
                           descriptor()
                               .id("cancel")
                               .name("reservation")
                               .type(Type.IDEMPOTENT)
                               .rt("#reservation")
                               .build()))
                   .collect(Collectors.toList()))
               .build()))
       .build());
}

分别是ALPS reservations集合资源概要文件的JSON表示,这基本上是客户端要处理的内容。

{
  "version": "1.0",
  "doc": {
    "href": "https://rentals.jcg.com/documentation.html"
  },
  "descriptor": [ {
    "id": "reservations",
    "type": "SEMANTIC",
    "descriptor": [ {
      "id": "book",
      "name": "reservations",
      "type": "UNSAFE",
      "rt": "#reservation"
    }, {
      "id": "list",
      "name": "reservations",
      "type": "SAFE",
      "rt": "#reservation"
    } ],
    "rt": "#reservation"
  }, {
    "id": "reservation",
    "type": "SEMANTIC",
    "descriptor": [ {
      "id": "from",
      "href" : "https://schema.org/Date",
      "type": "SEMANTIC"
    }, {
      "id": "id",
      "href" : "https://schema.org/Thing#identifier",
      "type": "SEMANTIC"
    }, {
      "id": "to",
      "href" : "https://schema.org/Date",
      "type": "SEMANTIC"
    }, {
      "id": "vehicle",
      "href" : "https://schema.org/Vehicle#name",
      "type": "SEMANTIC"
    }, {
      "id": "customer",
      "type": "SAFE",
      "rt": "#customer"
    }, {
      "id": "update",
      "name": "reservation",
      "type": "IDEMPOTENT",
      "rt": "#reservation"
    }, {
      "id": "cancel",
      "name": "reservation",
      "type": "IDEMPOTENT",
      "rt": "#reservation"
    } ]
  } ]
}

本着超媒体HATEOAS的精神, Spring HATEOAS方法也是面向资源的(或者更确切地说 ,面向资源表示的)。 基本上,您必须实现许多RepresentationModelAssembler (例如ReservationResourceAssembler )和控制器端点,这些端点依赖于各自的汇编器来构造单个资源表示或资源集合表示。

@Component
public class ReservationResourceAssembler implements SimpleRepresentationModelAssembler<Reservation> {
    @Override
    public void addLinks(EntityModel<Reservation> resource) {
        resource.add(linkTo(methodOn(CustomerController.class).findOne(resource.getContent().getCustomerId()))
            .withRel("customer")
            .withType(linkTo(methodOn(RootController.class).customers()).slash("#customer").toString()));
        resource.add(linkTo(methodOn(ReservationController.class).findOne(resource.getContent().getId()))
            .withSelfRel()
            .withType(linkTo(methodOn(RootController.class).reservations()).slash("#reservation").toString())
            .andAffordance(afford(methodOn(ReservationController.class).modify(resource.getContent().getId(), null)))
            .andAffordance(afford(methodOn(ReservationController.class).cancel(resource.getContent().getId()))));
    }
}

除了与customer的链接关系外,还有许多用于更改预留资源状态( modifycancel预留状态)的优惠(动作)。 另外,由于reservations收集资源正在使用分页(和排序),因此其表示的构造稍微复杂一点,并且涉及两个汇编程序,因此让我们看一下该示例。

@RestController
@RequestMapping(path = "/reservations")
public class ReservationController {
    @Autowired private ReservationRepository repository;
    @Autowired private ReservationResourceAssembler reservationResourceAssembler;
    @Autowired private PagedResourcesAssembler>Reservation< assembler;
    
    @GetMapping
    public ResponseEntity>PagedModel>EntityModel>Reservation<<< findAll(@PageableDefault Pageable pageable) {
        return ResponseEntity.ok(assembler.toModel(repository.findAll(pageable), reservationResourceAssembler));
    }
}

为了演示分页的效果,仅用说2个元素的页面大小来获取reservations集合就足够了。

{                                                                                        
  "_embedded": {                                                                        
    "reservations": [ {                                                                 
      "id": "13e1892765c5",                                                             
      "vehicle": "Honda Civic 2020",                                                    
      "from": "2020-01-01",                                                             
      "to": "2020-01-05",                                                               
      "_links": {                                                                       
        "customer": {                                                                   
          "href": "https://rentals.jcg.com/customers/fed195a03e9d",                       
          "type": "https://rentals.jcg.com/alps/customers#customer"                       
        },                                                                               
        "self": {                                                                       
          "href": "https://rentals.jcg.com/reservations/13e1892765c5",                    
          "type": "https://rentals.jcg.com/alps/reservations#reservation"                 
        }                                                                                
      },                                                                                 
      "_templates": {                                                                   
        "cancel": {                                                                     
          "method": "delete",                                                           
          "properties": [ ]                                                             
        },                                                                               
        "default": {                                                                    
          "method": "put",                                                              
          "properties": [ {                                                             
            "name": "from",                                                             
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "to",                                                               
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "vehicle",                                                          
            "required": true                                                            
          } ]                                                                            
        }                                                                                
      }                                                                                  
    }, {                                                                                 
      "id": "fc14e8ef90f5",                                                             
      "vehicle": "BMW 325i",                                                            
      "from": "2020-01-10",                                                             
      "to": "2020-01-12",                                                               
      "_links": {                                                                       
        "customer": {                                                                   
          "href": "https://rentals.jcg.com/customers/fed195a03e9d",                       
          "type": "https://rentals.jcg.com/alps/customers#customer"                       
        },                                                                               
        "self": {                                                                       
          "href": "https://rentals.jcg.com/reservations/fc14e8ef90f5",                    
          "type": "https://rentals.jcg.com/alps/reservations#reservation"                 
        }                                                                                
      },                                                                                 
      "_templates": {                                                                   
        "cancel": {                                                                     
          "method": "delete",                                                           
          "properties": [ ]                                                             
        },                                                                               
        "default": {                                                                    
          "method": "put",                                                              
          "properties": [ {                                                             
            "name": "from",                                                             
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "to",                                                               
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "vehicle",                                                          
            "required": true                                                            
          } ]                                                                            
        }                                                                                
      }                                                                                  
    } ]                                                                                  
  },                                                                                     
  "_links": {                                                                           
    "first": {                                                                          
      "href": "https://rentals.jcg.com/reservations?page=0&size=2"                        
    },                                                                                   
    "self": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=0&size=2"                        
    },                                                                                   
    "next": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=1&size=2"                        
    },                                                                                   
    "last": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=1&size=2"                        
    }                                                                                    
  },                                                                                     
  "page": {                                                                             
    "size": 2,                                                                          
    "totalElements": 3,                                                                 
    "totalPages": 2,                                                                    
    "number": 0                                                                         
  }                                                                                      
}

很容易发现附加的导航链接firstnextlast ,它们实际上是上下文相关的(例如,由于我们要求第一页,因此不存在prev链接关系)。

可以肯定地说Spring HATEOAS为JVM平台提供了最全面的超媒体HATEOAS支持。 尽管它没有立即实现某些规范,但它允许通过一组SPI 插入自定义媒体类型

九头蛇

决定采用JSON-LDHydraRESTful Web服务和API可能会受益于使用hydra-java库。 Spring HATEAOS扩展的出现非常令人鼓舞,但不幸的是,由于它不能与最新的Spring HATEOAS版本一起使用而被阴影掩盖

有了这个,我们对RESTful Web服务和API的服务器端实现有了一个很好的主意,现在该切换主题并讨论客户端。

4.在客户端上

从客户端角度,区分超媒体 API客户端的两个角度或类是有意义的:

  • (Web)用户界面(前端)上下文中的超媒体 API客户端
  • 业务任务实施(后端)环境中的超媒体 API客户端

JavaScript是Web前端开发的第一选择,而Java(通常是JVM)已经在后端方面占据了主导地位。 尽管我们将进一步讨论后者,但许多概念同样适用于两者。

那么, 超媒体 API客户端的设计和实现背后的原理是什么? 如果要强调的是一件事,那就是专注于针对超媒体规范进行编程,而不是检查服务器的响应。 媒体类型应向客户提供所有必要的详细信息,并作为实施指南。 而且,客户可能只使用一些特定的流程,而无需实现服务必须提供的所有功能。

JAX-RS

JAX-RS 2.1规范包括客户端部分,不幸的是,该客户端部分仅提供了从Link头中提取链接的方法。

final Client client = ClientBuilder
    .newClient();

try (final Response response = client
        .target("https://rentals.jcg.com/")
        .request()
        .accept("application/prs.hal-forms+json")
        .get()) {
                    
    final Link customers = response.getLink("customers");
    if (customers != null) {
        // follow the link here 
    }
} finally {
    client.close();
}

基本上,与服务器端一样 ,如果您需要完成某些工作,请准备好卷起袖子。

Crnk

Crnk框架提供了很好的客户端支持,并通过熟悉的构建块实现了这些构建块:资源,关系和存储库。

final CrnkClient client = new CrnkClient("https://rentals.jcg.com/");
client.setHttpAdapter(new OkHttpAdapter());
        
final ResourceRepository>Customer, String< repository = client.getRepositoryForType(Customer.class);

final ResourceRepository>Customer, String< repository = client.getRepositoryForType(Customer.class);
final List>Customer< customers = repository.findAll(
    new QuerySpec(Customer.class)
        .setPaging(new OffsetLimitPagingSpec(0L, 10L)));

if (!customers.isEmpty()) {
    // navigate through customers
}

如果您的RESTful Web服务和API遵循JSON:API规范,则Crnk客户端可以为您节省大量时间和精力。

Spring帽子

令人惊讶的是,直到最近SpringHATEOAS超媒体客户端的支持还有些不完整,但是最新版本带来了许多改进TraversonSpring HATEOAS支持的最古老的机制,用于在链接和关系之间导航。

final RestTemplate template = ...;
            
final Map>String, Object< paging = Map.of(
        "page", 0L,
        "size", 2L
    );
    
final CollectionModelType>Reservation< resourceType =
    new TypeReferences.CollectionModelType>Reservation<() {};
            
final Traverson traverson = new Traverson(URI.create("https://rentals.jcg.com/"), MediaTypes.HAL_FORMS_JSON)
    .setLinkDiscoverers(List.of(new HalFormsLinkDiscoverer()))
    .setRestOperations(template);
            
final CollectionModel>Reservation< reservations = traverson
    .follow(rel("reservations").withParameters(paging))
    .toObject(resourceType);;
                
if (!reservations.getContent().isEmpty()) {
    // navigate through reservations
}

WebClientRestTemplate这样的更传统的通用API客户端(我们称它们为REST客户端)也已经得到了超媒体支持。

final WebClient client = builder.build();
                
final CollectionModelType>Reservation< resourceType =
    new TypeReferences.CollectionModelType>Reservation<() {};
final LinkDiscoverer discoverer = new HalFormsLinkDiscoverer();
    
final Optional>Link< link = client
    .get()
    .uri("https://rentals.jcg.com/")
    .accept(MediaTypes.HAL_FORMS_JSON)
    .retrieve()
    .bodyToMono(String.class)
    .map(r -< discoverer.findLinkWithRel("reservations", r))
    .block();
            
if (link.isPresent()) {
    final Map>String, Object< paging = Map.of(
            "page", 0L,
            "size", 2L
        );

    final URI uri = link
        .get()
        .getTemplate()
        .expand(paging);
                
     final CollectionModel>Reservation< reservations = client
        .get()
        .uri(uri)
        .accept(MediaTypes.HAL_FORMS_JSON)
        .retrieve()
        .bodyToMono(resourceType)
        .block();
                                
    if (!reservations.getContent().isEmpty()) {
        // navigate through reservations
    }
}

甚至可以选择TraversonRestTemplateWebClient ,当然可以实现功能强大的超媒体 API客户端,以完全自动化服务工作流程。

5。结论

在本教程的这一部分中,我们讨论了可帮助您在JVM平台上设计和实现超媒体驱动的RESTful Web API及其客户端的库和框架。 尽管数量不多,但您可以选择。

由于它主要是使用JavaScript在浏览器端完成的,因此我们跳过了在Web前端上下文中有关超媒体 API客户端的讨论。 在这方面,仍然值得一提的是Traverson ,它是Node.js和浏览器的超媒体 API / HATEOAS客户端(是的,这是Spring HATEAOS的灵感来源)。

6.接下来

在接下来的本教程的最后一部分,我们将总结REST体系结构样式中最被遗忘和神秘的约束( 超媒体作为应用程序状态引擎( HATEOAS ))背后的理论和实践。

7.下载源代码

下载
您可以在此处下载此示例的完整源代码: 具有HATEOAS的RESTful服务:JVM上的REST API和超媒体

翻译自: https://www.javacodegeeks.com/restful-services-with-hateoas-rest-apis-and-hypermedia-on-jvm.html

restful和rest

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值