1. 概述
本文將重點介紹在 Spring REST 服務中實現可發現性的實施,以及滿足 HATEOAS 約束。本文重點介紹 Spring MVC。我們的文章
2. 通過事件解耦可發現性
首先,讓我們創建事件:
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;
}
}
然後,
@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));
}
}
監聽器應該是調用堆棧中最後一個對象,並且不需要直接訪問它們;因此它們不是公開的。
3. 創建新資源的 URI 可發現性
如前文所述,創建新資源的操作應在響應的 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”關係類型,該類型已指定並用於多個微格式,但尚未標準化。
“Link” 頭部是用於可發現性的最常用的 HTTP 頭部之一
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,而不是依賴硬編碼值。 另一方面——但這並不是 Web 的工作方式:是的,URI 被發現,但它們也被書籤。
一個微妙但重要的區別是 API 的演化——舊的 URI 仍然有效,但任何發現 API 的客户端都應該發現新的 URI——這允許 API 動態變化,並且好的客户端即使在 API 發生變化時也能正常工作。
總之——所有 RESTful Web 服務 URI 都應該被考慮 酷 URI 規範 (以及酷 URI 不改變)——但這並不意味着遵循 HATEOAS 約束在 API 演化時仍然非常有益。
6. 發現性的注意事項
正如之前文章中討論的,發現性的首要目標是儘量減少或不使用文檔,並讓客户端通過獲得的響應來學習和理解如何使用 API。
事實上,這不應被視為一個遙遠的目標——我們消費每一個新的網頁都是沒有文檔的。因此,如果這個概念在 REST 的上下文中更具問題性,那麼它必須是一個技術實現的問題,而不是關於是否有可能的問題。
儘管如此,從技術上講,我們仍然距離一個完全可行的解決方案很遠——規範和框架支持仍在不斷髮展,因此我們必須做出一些妥協。
7. 結論
本文介紹了在 RESTful 服務中使用 Spring MVC 以及根級別的可發現性概念時,可發現性的實施情況。