案例概述
本文将重点介绍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服务的上下文中实现的一些可发现性特征,并从根本上触及了可发现性的概念。
评论区