在Spring MVC REST服务中使用HATEOAS

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ShrCheng/article/details/81325849

在Spring MVC REST服务中使用HATEOAS

案例概述

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

通过事件解耦可发现性

可发现性作为Web层一个单独的方面或关注点应该与处理HTTP请求的控制器分离。为此,Controller将触发所有需要额外操作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;
    }
}
  • 然后,Controller**有2个简单的操作 - 通过id和create 查找:**
@Controller
@RequestMapping(value = "/foos")
public class FooController {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private IFooService service;

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

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

    @RequestMapping(method = RequestMethod.POST)
    @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约束的方向发展。

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

使新创建资源的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 - 这是在Spring 3.1中引入的,以帮助使用当前的Request。这样,我们不需要传递任何东西,我们可以简单地静态访问它。

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

获取单一资源

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

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

对于可发现性而言,链接头是最常用的HTTP报头之一。创建此标头的实用程序非常简单:

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

根的可发现性

根是整个服务的入口点 - 这是客户第一次使用API​​时所接触到的内容。如果要在整个过程中考虑并实施HATEOAS约束,那么这就是开始的地方。事实上系统的所有主要URI都必须从根目录中发现,这一点不应该让人感到意外。

现在让我们看看控制器:

@RequestMapping( value = "admin",method = RequestMethod.GET )
@ResponseStatus( value = HttpStatus.NO_CONTENT )
public void adminRoot( HttpServletRequest request, HttpServletResponse response ){
    String rootUri = request.getRequestURL().toString();

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

这当然只是一个概念例子,侧重于Foo资源的单个样本URI - 一个真正的实现应该为发布到客户端的所有资源添加类似的URI。

可发现性与更改URI无关

这可能是一个有争议的问题 - 一方面,HATOAS的目的是让客户端发现API的URI而不依赖于硬编码值。另一方面 - 这不是网络的工作原理:是的,URI被发现,但它们也被加入书签。

一个微妙但重要的区别是API的演变 - 旧的URI应该仍然有用,但是任何发现API的客户端都应该发现新的URI - 它允许API动态地改变,并且优秀的客户端即使在API时也能正常工作变化。

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

可发现性的注意事项

正如前面文章中的一些讨论所述,可发现性的第一个目标是尽可能少地使用文档,让客户通过其获得的响应来学习和理解如何使用API​​。事实上,这不应该被视为一个遥不可及的理想 - 它是我们如何使用每个新网页 - 没有任何文档的情况下。因此,如果概念在REST环境中更有问题,那么它必须是技术实现的问题,而不是它是否可能的问题。

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

案例结论

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

猜你喜欢

转载自blog.csdn.net/ShrCheng/article/details/81325849