簡介
API文件是建立REST API時不可或缺的一部分。 SpringDoc OpenAPI 會自動從程式碼產生 API 文檔,而 HATEOAS 透過超媒體連結幫助 API 進行自我描述。
1.SpringDoc OpenAPI
1.1 設置
// build.gradle.kts
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0")
# application.yml
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: method
1.2 配置OpenAPI元數據
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("My App API")
.version("1.0.0")
.description("REST API documentation for My App")
.contact(new Contact()
.name("Dev Team")
.email("[email protected]"))
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")))
.addSecurityItem(new SecurityRequirement()
.addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.bearerFormat("JWT")
.scheme("bearer")));
}
}
1.3 註解控制器
@RestController
@RequestMapping("/api/v1/products")
@Tag(name = "Products", description = "Product management APIs")
public class ProductController {
@Operation(
summary = "Get product by ID",
description = "Returns a single product by its ID"
)
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Product found",
content = @Content(schema =
@Schema(implementation = ProductResponse.class))),
@ApiResponse(responseCode = "404",
description = "Product not found")
})
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> getProduct(
@Parameter(description = "Product ID", example = "1")
@PathVariable Long id) {
return ResponseEntity.ok(productService.getProduct(id));
}
@Operation(summary = "Search products with filters")
@GetMapping
public ResponseEntity<Page<ProductResponse>> searchProducts(
@Parameter(description = "Search keyword")
@RequestParam(required = false) String keyword,
@Parameter(description = "Min price")
@RequestParam(required = false) BigDecimal minPrice,
@ParameterObject Pageable pageable) {
return ResponseEntity.ok(
productService.search(keyword, minPrice, pageable));
}
}
1.4 群組API
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/v1/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.addOpenApiMethodFilter(method ->
method.isAnnotationPresent(PreAuthorize.class))
.build();
}
訪問:
- 招搖使用者介面:
http://localhost:8080/swagger-ui.html - JSON 文檔:
http://localhost:8080/api-docs
2. HATEOAS — 超媒體作為應用程式狀態的引擎
2.1 設置
// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-hateoas")
2.2 建立資源模型
public class ProductModel extends RepresentationModel<ProductModel> {
private Long id;
private String name;
private BigDecimal price;
private String category;
// constructors, getters
}
2.3 新增回應鏈接
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@GetMapping("/{id}")
public EntityModel<ProductResponse> getProduct(@PathVariable Long id) {
ProductResponse product = productService.getProduct(id);
return EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id))
.withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(Pageable.unpaged()))
.withRel("products"),
linkTo(methodOn(ReviewController.class).getReviews(id))
.withRel("reviews")
);
}
@GetMapping
public CollectionModel<EntityModel<ProductResponse>> getAllProducts(
Pageable pageable) {
Page<ProductResponse> products = productService.getAll(pageable);
List<EntityModel<ProductResponse>> productModels = products.stream()
.map(product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class)
.getProduct(product.id())).withSelfRel()))
.toList();
return CollectionModel.of(productModels,
linkTo(methodOn(ProductController.class)
.getAllProducts(pageable)).withSelfRel());
}
}
2.4 HAL+JSON 格式的回應
{
"id": 1,
"name": "Spring Boot in Action",
"price": 450000,
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/products/1"
},
"products": {
"href": "http://localhost:8080/api/v1/products"
},
"reviews": {
"href": "http://localhost:8080/api/v1/products/1/reviews"
}
}
}
2.5 表示模型組裝器
@Component
public class ProductModelAssembler
implements RepresentationModelAssembler<Product, EntityModel<ProductResponse>> {
@Override
public EntityModel<ProductResponse> toModel(Product entity) {
ProductResponse response = ProductResponse.from(entity);
return EntityModel.of(response,
linkTo(methodOn(ProductController.class)
.getProduct(entity.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class)
.getAllProducts(Pageable.unpaged())).withRel("products"));
}
}
3. API 版本控制策略
| 戰略 | 範例 | 優點 | 缺點 |
|---|---|---|---|
| 網址路徑 | /api/v1/產品 | 清晰、易快取 | 更改網址 |
| 查詢參數 | /api/產品?版本=1 | 靈活 | 容易忘記 |
| 標題 | 接受:application/vnd.api.v1+json | 乾淨的網址 | 使用瀏覽器測試困難 |
| 媒體類型 | 內容類型:application/vnd.api.v1+json | 最寧靜 | 複雜 |
建議:URL 路徑版本控制(/api/v1/)是最簡單也是最流行的。
總結
- SpringDoc OpenAPI 自動從控制器註釋產生 Swagger UI 和 JSON 文件
- HATEOAS 在回應中添加超媒體鏈接,幫助客戶無需文件即可探索 API
- 大多數情況下,API 版本控制應使用 URL 路徑 (/api/v1/)
練習
- SpringDoc OpenAPI整合:使用@Operation、@ApiResponse註釋所有控制器,為公共和管理API建立分組文檔 2.為產品API實現HATEOAS:每個產品響應包含自鏈接、收藏鏈接和相關資源鏈接 3.為ProductController建立API版本控制:/api/v1和/api/v2,v2有附加欄位並更改了回應格式