HATEOAS Link 使用指南
Package:
io.leandev.appfuse.web.link.*狀態: 穩定
簡介
LinkBuilder 是 AppFuse Server 提供的 HATEOAS 連結建構工具,採用相對路徑設計,專為 Reverse Proxy 和雲原生架構優化。
核心特色
| 特色 | 說明 |
|---|---|
| 相對路徑 | 不含 host/port,支援 Reverse Proxy |
| Builder 模式 | 流暢的鏈式 API |
| 不可變設計 | 執行緒安全 |
| Spring 整合 | 使用 Spring HATEOAS Link |
為何使用相對路徑?
// AppFuse LinkBuilder 生成相對路徑
"api/users/123"
// 傳統做法生成絕對 URL
"http://localhost:8080/api/users/123"
相對路徑優勢:
- ✅ 支援 Reverse Proxy 架構
- ✅ 無需配置 Forwarded Headers
- ✅ 同一回應適用所有環境(dev/staging/prod)
- ✅ 避免暴露內部網路資訊
快速開始
基本使用
import io.leandev.appfuse.web.link.LinkBuilder;
import org.springframework.hateoas.Link;
// 基於 Controller 建立連結
Link selfLink = LinkBuilder.linkTo(UserController.class)
.slash(123)
.withSelfRel();
// 結果: Link{rel='self', href='api/users/123'}
完整範例
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserResource> getUser(@PathVariable Long id) {
User user = userService.findById(id);
UserResource resource = toResource(user);
return ResponseEntity.ok(resource);
}
private UserResource toResource(User user) {
UserResource resource = new UserResource(user);
// 自我連結
resource.add(
LinkBuilder.linkTo(UserController.class)
.slash(user.getId())
.withSelfRel()
);
// 相關資源連結
resource.add(
LinkBuilder.linkTo(UserController.class)
.slash(user.getId())
.slash("orders")
.withRel("orders")
);
// 集合資源連結
resource.add(
LinkBuilder.linkTo(UserController.class)
.withRel("all-users")
);
return resource;
}
}
JSON 回應範例
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "api/users/123" },
"orders": { "href": "api/users/123/orders" },
"all-users": { "href": "api/users" }
}
}
核心 API
靜態工廠方法
LinkBuilder.linkTo(Class<?> controller)
自動發現 Controller 的 @RequestMapping 路徑作為基礎 URI。
slash(Object path)
添加路徑段:
LinkBuilder.linkTo(UserController.class)
.slash("123") // api/users/123
.slash("orders") // api/users/123/orders
.slash("latest") // api/users/123/orders/latest
withContext(String context)
添加可選的 context 前綴:
LinkBuilder.linkTo(UserController.class)
.withContext("v1")
.slash("123")
.withSelfRel();
// 結果: v1/api/users/123
適用場景:API 版本隔離、多應用共享域名、租戶路徑隔離
withSelfRel() / withRel(String rel)
生成連結:
Link selfLink = builder.withSelfRel(); // rel="self"
Link ordersLink = builder.withRel("orders"); // rel="orders"
toUri() / toHref()
URI uri = builder.toUri(); // URI 物件
String href = builder.toHref(); // 字串 "api/users/123"
常見場景
場景 1: 多層路徑
// 使用者的訂單中的特定商品
Link link = LinkBuilder.linkTo(UserController.class)
.slash(userId)
.slash("orders")
.slash(orderId)
.slash("items")
.slash(itemId)
.withSelfRel();
// 結果: api/users/123/orders/456/items/789
場景 2: 巢狀資源
@GetMapping("/{userId}/orders")
public ResponseEntity<List<OrderResource>> getUserOrders(@PathVariable Long userId) {
List<Order> orders = orderService.findByUserId(userId);
List<OrderResource> resources = orders.stream()
.map(order -> {
OrderResource resource = new OrderResource(order);
// 訂單的 self 連結
resource.add(
LinkBuilder.linkTo(UserController.class)
.slash(userId)
.slash("orders")
.slash(order.getId())
.withSelfRel()
);
// 返回到使用者
resource.add(
LinkBuilder.linkTo(UserController.class)
.slash(userId)
.withRel("user")
);
return resource;
})
.collect(Collectors.toList());
return ResponseEntity.ok(resources);
}
場景 3: 分頁連結
// self 連結
resource.addLink(LinkBuilder.linkTo(controller).withSelfRel());
// 下一頁
if (page.hasNext()) {
resource.addLink(
LinkBuilder.linkTo(controller)
.slash("?page=" + (page.getNumber() + 1))
.withRel("next")
);
}
// 上一頁
if (page.hasPrevious()) {
resource.addLink(
LinkBuilder.linkTo(controller)
.slash("?page=" + (page.getNumber() - 1))
.withRel("prev")
);
}
前端整合
React/TypeScript
interface Link {
href: string;
}
interface UserResource {
id: number;
name: string;
_links: {
self: Link;
orders: Link;
};
}
async function fetchUser(userId: string): Promise<UserResource> {
const response = await fetch(`/api/users/${userId}`);
const user: UserResource = await response.json();
// 直接使用相對連結
const ordersResponse = await fetch(user._links.orders.href);
const orders = await ordersResponse.json();
return user;
}
// 或使用 URL API 明確組合
function getFullUrl(link: Link, baseUrl: string): string {
return new URL(link.href, baseUrl).toString();
}
與 Spring WebMvcLinkBuilder 的差異
| 特性 | Spring WebMvcLinkBuilder | AppFuse LinkBuilder |
|---|---|---|
| 方法引用 | ✅ methodOn() | ❌ 僅支援 Class |
| 路徑建構 | 自動提取 | 手動 slash() |
| 輸出格式 | 絕對 URL | 相對路徑 |
| Reverse Proxy | 需配置 | 原生支援 |
為何自建?
- 輕量級:避免完整 Spring MVC 依賴
- 雲原生:相對路徑更適合容器化部署
- 彈性:整合 Apache URIBuilder 提供更多控制
常見問題
Q: 前端如何處理相對路徑?
A: 瀏覽器的 fetch() API 會自動基於當前頁面 URL 解析。或使用 new URL(relativeUrl, baseUrl) 明確組合。
Q: 如何處理查詢參數?
A: 目前可通過 slash() 手動添加:
LinkBuilder.linkTo(UserController.class)
.slash("?page=2&size=10")
.withSelfRel()
API 參考
詳細的類別設計,請參閱 API 參考: Link 或 Javadoc。