基於 Spring REST 服務的 HATEOAS

REST,Spring
Remote
0
09:01 PM · Dec 01 ,2025

1. 概述本文將重點介紹在 Spring REST 服務中實現可發現性的實施,以及滿足 HATEOAS 約束。

本文重點介紹 Spring MVC。我們的文章描述瞭如何在 Spring Boot 中使用 HATEOAS。

2. 通過事件解耦可發現性

可發現性應作為 Web 層的一個獨立方面或關注點,與處理 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;
    }
}

然後,控制器具有 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 可發現性如前文所述,創建新資源的操作應在響應的 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 以及根級別的可發現性概念時,可發現性的實施情況。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.