HATEOAS for a Spring REST Service – 用于Spring REST服务的HATEOAS

最后修改: 2011年 11月 13日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

This article will focus on the implementation of discoverability in a Spring REST Service and on satisfying the HATEOAS constraint.

本文将重点讨论在Spring REST服务中实现可发现性以及满足HATEOAS约束的问题。

This article focuses on Spring MVC. Our article An Intro to Spring HATEOAS describes how to use HATEOAS in Spring Boot.

本文重点介绍Spring MVC。我们的文章An Intro to Spring HATEOAS介绍了如何在Spring Boot中使用HATEOAS。

2. Decoupling Discoverability Through Events

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

Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller handling the HTTP request. For this purpose, the Controller will fire off events for all the actions that require additional manipulation of the response.

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

First, let’s create the events:

首先,我们来创建事件。

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;
    }
}

Then, the Controller, with 2 simple operations – find by id and create:

然后,控制器,有两个简单的操作–通过id查找创建

@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));
    }
}

We can then handle these events with any number of decoupled listeners. Each of these can focus on its own particular case and help towards satisfying the overall HATEOAS constraint.

然后我们可以通过任何数量的解耦监听器来处理这些事件。每个监听器都可以专注于自己的特定情况,并帮助满足整个HATEOAS约束。

The listeners should be the last objects in the call stack and no direct access to them is necessary; as such they are not public.

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

3. Making the URI of a Newly Created Resource Discoverable

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

As discussed in the previous post on HATEOAS, the operation of creating a new Resource should return the URI of that resource in the Location HTTP header of the response.

正如在HATEOAS 上一篇文章中所讨论的那样,创建新资源的操作应在响应的Location HTTP 头中返回该资源的 URI

We’ll handle this using a listener:

我们将使用一个监听器来处理这个问题。

@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());
    }
}

In this example, we’re making use of the ServletUriComponentsBuilder – which helps with using the current Request. This way, we don’t need to pass anything around and we can simply access this statically.

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

If the API would return ResponseEntity – we could also use the Location support.

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

4. Getting a Single Resource

4.获得单一的资源

On retrieving a single Resource, the client should be able to discover the URI to get all Resources of that type:

在检索单个资源时,客户应该能够发现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);
    }
}

Note that the semantics of the link relation make use of the “collection” relation type, specified and used in several microformats, but not yet standardized.

请注意,链接关系的语义利用了“collection”关系类型,该类型在若干微格式中被指定和使用,但尚未标准化。

The Link header is one of the most used HTTP headers for the purposes of discoverability. The utility to create this header is simple enough:

链接头是最常用的HTTP头之一为了可发现性的目的。创建这个头的工具很简单。

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

5. Discoverability at the Root

5.根源上的可发现性

The root is the entry point in the entire service – it’s what the client comes into contact with when consuming the API for the first time.

根是整个服务的入口–它是客户第一次消费API时接触到的东西。

If the HATEOAS constraint is to be considered and implemented throughout, then this is the place to start. Therefore all the main URIs of the system have to be discoverable from the root.

如果HATEOAS约束要被考虑并贯穿实施,那么这就是开始的地方。因此,该系统的所有主要URI都必须可以从根部发现。

Let’s now look at the controller for this:

现在让我们来看看这个的控制器。

@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);
}

This is, of course, an illustration of the concept, focusing on a single, sample URI, for Foo Resources. A real implementation should add, similarly, URIs for all the Resources published to the client.

当然,这只是对这一概念的说明,主要是针对Foo资源的单个示例URI。真正的实现应该添加类似的URI,用于发布给客户端的所有资源。

5.1. Discoverability Is Not About Changing URIs

5.1.可发现性不是关于改变URI的问题

This can be a controversial point – on the one hand, the purpose of HATEOAS is to have the client discover the URIs of the API and not rely on hardcoded values. On the other hand – this is not how the web works: yes, URIs are discovered, but they are also bookmarked.

这可能是一个有争议的观点–一方面,HATEOAS的目的是让客户发现API的URI,而不是依赖硬编码的值。另一方面–这不是网络的工作方式:是的,URI是被发现的,但它们也是被收藏的。

A subtle but important distinction is the evolution of the API – the old URIs should still work, but any client that will discover the API should discover the new URIs – which allows the API to change dynamically, and good clients to work well even when the API changes.

一个微妙但重要的区别是API的演变–旧的URI应该仍然有效,但任何会发现API的客户端都应该发现新的URI–这使得API可以动态变化,而好的客户端即使在API变化时也能很好地工作。

In conclusion – just because all URIs of the RESTful web service should be considered cool URIs (and cool URIs don’t change) – that doesn’t mean that adhering to the HATEOAS constraint isn’t extremely useful when evolving the API.

总之–只是因为RESTful网络服务的所有URI应该被认为是cool。w3.org/TR/cooluris/” rel=”nofollow noopener noreferrer” target=”_blank” title=”酷的URI规范”>URI(而酷的URI 不改变)- 但这并不意味着在发展API时遵守HATEOAS的约束就不那么有用了。

6. Caveats of Discoverability

6.可发现性的注意事项

As some of the discussions around the previous articles state, the first goal of discoverability is to make minimal or no use of documentation and have the client learn and understand how to use the API via the responses it gets.

正如围绕之前文章的一些讨论所指出的,可发现性的第一个目标是尽量少用或不用文档,让客户通过得到的响应来学习和理解如何使用API。

In fact, this shouldn’t be regarded as such a far fetched ideal – it’s how we consume every new web page – without any documentation. So, if the concept is more problematic in the context of REST, then it must be a matter of technical implementation, not of a question of whether or not it’s possible.

事实上,这不应该被视为一个如此牵强的理想–我们就是这样消费每一个新的网页的–没有任何文档。因此,如果这个概念在REST的背景下更有问题,那么这一定是一个技术实现的问题,而不是一个是否可能的问题。

That being said, technically, we are still far from a fully working solution – the specification and framework support are still evolving, and because of that, we have to make some compromises.

也就是说,从技术上讲,我们离一个完全可行的解决方案还很远–规范和框架支持仍在不断发展,正因为如此,我们不得不做出一些妥协。

7. Conclusion

7.结论

This article covered the implementation of some of the traits of discoverability in the context of a RESTful Service with Spring MVC and touched on the concept of discoverability at the root.

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

The implementation of all these examples and code snippets can be found over on GitHub – this is a Maven-based project, so it should be easy to import and run as it is.

所有这些例子和代码片段的实现都可以在GitHub上找到over – 这是一个基于Maven的项目,因此应该很容易导入并按原样运行。