阅读 24

在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服务的上下文中实现的一些可发现性特征,并从根本上触及了可发现性的概念。


作者:EdurtIO
链接:https://juejin.cn/post/7167888676645502983


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐