用于Spring REST服务的HATEOAS

2023/05/12

1. 概述

本文将重点关注Spring REST服务中可发现性的实现以及满足HATEOAS约束。

本文主要关于Spring MVC。我们的文章Spring HATEOAS简介描述了如何在Spring Boot中使用HATEOAS。

2. 通过事件解耦可发现性

可发现性作为Web层的一个单独方面或关注点应该与处理HTTP请求的控制器分离。为此,控制器将为需要对响应进行额外操作的所有操作触发事件。

首先,让我们创建事件:

public class SingleResourceRetrieved extends ApplicationEvent {
    private HttpServletResponse response;

    public SingleResourceRetrieved(Object source, HttpServletResponse response) {
        super(source);

        this.response = response;
    }

    public HttpServletResponse getResponse() {
        return response;
    }
}

public class ResourceCreated extends ApplicationEvent {
    private HttpServletResponse response;
    private long idOfNewResource;

    public ResourceCreated(Object source, HttpServletResponse response, long idOfNewResource) {
        super(source);

        this.response = response;
        this.idOfNewResource = idOfNewResource;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public long getIdOfNewResource() {
        return idOfNewResource;
    }
}

然后,控制器包含两个简单的操作-findById和create:

@RestController
@RequestMapping(value = "/foos")
public class FooController {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private IFooService service;

    @GetMapping(value = "foos/{id}")
    public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
        Foo resourceById = Preconditions.checkNotNull(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrieved(this, response));
        return resourceById;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void create(@RequestBody Foo resource, HttpServletResponse response) {
        Preconditions.checkNotNull(resource);
        Long newId = service.create(resource).getId();

        eventPublisher.publishEvent(new ResourceCreated(this, response, newId));
    }
}

然后我们可以使用任意数量的解耦监听器来处理这些事件。其中每一个都可以专注于自己的特定情况,并有助于满足整体HATEOAS约束。

监听器应该是调用栈中的最后一个对象,不需要直接访问它们;因此,它们不是公开的。

3. 使新创建的资源的URI可发现

正如之前关于HATEOAS的文章中所讨论的,创建新资源的操作应该在响应的Location HTTP标头中返回该资源的URI。

我们将使用监听器处理此问题:

@Component
class ResourceCreatedDiscoverabilityListener implements ApplicationListener<ResourceCreated>{

    @Override
    public void onApplicationEvent(ResourceCreated resourceCreatedEvent){
        Preconditions.checkNotNull(resourceCreatedEvent);

        HttpServletResponse response = resourceCreatedEvent.getResponse();
        long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();

        addLinkHeaderOnResourceCreation(response, idOfNewResource);
    }

    void addLinkHeaderOnResourceCreation(HttpServletResponse response, long idOfNewResource){
        URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().
              path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
        response.setHeader("Location", uri.toASCIIString());
    }
}

在这个例子中,我们使用了ServletUriComponentsBuilder-这有助于使用当前的请求。这样,我们不需要传递任何东西,我们可以简单地静态访问它。

如果API返回ResponseEntity-我们也可以使用Location支持

4. 获取单个资源

在检索单个资源时,客户端应该能够发现URI以获取该类型的所有资源:

@Component
class SingleResourceRetrievedDiscoverabilityListener implements ApplicationListener<SingleResourceRetrieved>{

    @Override
    public void onApplicationEvent(SingleResourceRetrieved resourceRetrievedEvent){
        Preconditions.checkNotNull(resourceRetrievedEvent);

        HttpServletResponse response = resourceRetrievedEvent.getResponse();
        addLinkHeaderOnSingleResourceRetrieval(request, response);
    }

    void addLinkHeaderOnSingleResourceRetrieval(HttpServletResponse response){
        String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri().
              build().toUri().toASCIIString();
        int positionOfLastSlash = requestURL.lastIndexOf("/");
        String uriForResourceCreation = requestURL.substring(0, positionOfLastSlash);

        String linkHeaderValue = LinkUtil
              .createLinkHeader(uriForResourceCreation, "collection");
        response.addHeader(LINK_HEADER, linkHeaderValue);
    }
}

请注意,链接关系的语义使用“collection”关系类型,它在多种微格式中指定和使用,但尚未标准化。

Link标头是用于可发现性的最常用的HTTP标头之一。创建此标头的实用程序非常简单:

public class LinkUtil {
    public static String createLinkHeader(String uri, String rel) {
        return "<" + uri + ">; rel=\"" + rel + "\"";
    }
}

5. 根部的可发现性

根是整个服务的入口点-这是客户端在第一次使用API时接触到的。

如果要在整个过程中考虑并实施HATEOAS约束,那么这就是起点。因此,系统的所有主要URI都必须可以从根目录中发现。

现在让我们看一下控制器:

@GetMapping("/")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) {
    String rootUri = request.getRequestURL().toString();

    URI fooUri = new UriTemplate("{rootUri}{resource}").expand(rootUri, "foos");
    String linkToFoos = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection");
    response.addHeader("Link", linkToFoos);
}

当然,这是对概念的说明,重点是Foo资源的单个示例URI。类似地,真正的实现应该为发布给客户端的所有资源添加URI。

5.1 可发现性与更改URI无关

这可能是一个有争议的点-一方面,HATEOAS的目的是让客户端发现API的URI,而不是依赖于硬编码值。另一方面-这不是Web的工作方式:是的,URI被发现了,但它们也被添加了书签。

一个微妙但重要的区别是API的演变-旧的URI应该仍然有效,但是任何发现API的客户端都应该发现新的URI,这允许API动态变化,即使API发生变化,好的客户端也能很好地工作。

总之-仅仅因为RESTful Web服务的所有URI都应被视为URI(并且酷URI不会改变)——这并不意味着在发展API时遵守HATEOAS约束不是非常有用

6. 可发现性警告

正如之前文章的一些讨论所述,可发现性的首要目标是尽量减少或不使用文档,并让客户端通过获得的响应学习和理解如何使用API。

事实上,这不应该被视为如此遥不可及的理想-这就是我们在没有任何文档的情况下使用每个新网页的方式。所以,如果这个概念在REST的上下文中更有问题,那么它一定是技术实现的问题,而不是它是否可能的问题。

话虽这么说,从技术上讲,我们离一个完全可行的解决方案还很远-规范和框架支持仍在不断发展,因此,我们必须做出一些妥协。

7. 总结

本文介绍了在使用Spring MVC的RESTful服务的上下文中可发现性的一些特征的实现,并从根本上触及了可发现性的概念。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章