知識庫 / Spring RSS 訂閱

基於 Spring RESTful 服務設計的 HATEOAS 模式

REST,Spring
HongKong
10
04:12 AM · Dec 06 ,2025

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 和根級可發現性概念背景下,可發現性的實施方法。

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

發佈 評論

Some HTML is okay.