1. 概述
本文將重點介紹在 Spring REST 服務中實現可發現性的方法,並滿足 HATEOAS 約束。
本文重點介紹 Spring MVC。我們的文章《Spring HATEOAS 入門》描述瞭如何在 Spring Boot 中使用 HATEOAS。
2. 通過事件解耦可發現性
可發現性應作為與 HTTP 請求處理的控制器分開的 Web 層面的獨立方面或關注點。為此,控制器將為所有需要對響應進行額外處理的動作觸發事件。
首先,讓我們創建事件:
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;
}
}然後,控制器具有 2 個簡單操作 – 按 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));
}
}我們可以使用任意數量的解耦監聽器來處理這些事件。 每一個監聽器都可以專注於自身的情況,並幫助滿足整體的 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 —— 它有助於使用當前的 Request。 這樣,我們就不需要傳遞任何內容,並且可以靜態地訪問它。
如果 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” 關係類型,該類型已在 多個微格式中被指定和使用,但尚未進行標準化。
HTTP 頭部中的 “Link” 頭部是最常用的頭部之一,用於提高可發現性。 創建此頭部的功能非常簡單:
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);
}這當然是一個概念的演示,重點關注一個單獨的、示例 URI,用於 Foo 資源。 實際的實現應該同樣添加,所有發佈到客户端的資源對應的 URI。
5.1. 發現性並非改變URI的途徑
這是一個可能引起爭議的觀點——一方面,HATEOAS 的目的在於讓客户端發現 API 的 URI,而不是依賴硬編碼的值。另一方面——這並不是互聯網的工作方式:是的,URI 被發現,但它們也被書籤收藏。
一個微妙但重要的區分在於 API 的演化——舊的 URI 仍然應該有效,但任何發現 API 的客户端都應該發現新的 URI——這使得 API 可以動態變化,並且好的客户端即使在 API 發生變化時也能正常工作。
總之——僅僅因為所有 RESTful Web 服務中的 URI 都應該被認為是 酷 URI規範,不改變)——並不意味着遵守 HATEOAS 約束在 API 演化過程中仍然非常有益。
6. 可發現性的注意事項
正如之前文章討論中提到的,可發現性的首要目標是儘可能減少或不使用文檔,並讓客户端通過獲取的響應來學習和理解如何使用 API。
事實上,這不應該被視為一個遙遠的目標——我們消費每一個新的網頁的方式就是沒有文檔。因此,如果這個概念在 REST 的上下文中更具問題性,那麼這必須是一個技術實現的問題,而不是關於是否有可能的問題。
儘管如此,從技術角度來看,我們距離一個完全可行的解決方案仍然很遠——規範和框架支持仍在不斷髮展,因此我們必須做出一些妥協。
7. 結論
本文介紹了在基於 RESTful 服務的 Spring MVC 和根級可發現性概念背景下,可發現性的實施方法。