Chuyển đến nội dung chính

第 16 課:使用 OpenAPI 和 HATEOAS 編寫 API 文檔

SpringDoc OpenAPI 3 — 自動產生 Swagger UI 和 API 文件。 HATEOAS 和超媒體驅動的 API。 API 版本控制策略和最佳實務。

💻 程式設計 — 第 15 課 第 16 課:使用 OpenAPI 進行 API 文件 & 哈特奧阿斯

Spring Boot 4:從基礎到高級

第 4 部分:進階功能

亞洲開發網

簡介

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/)

練習

  1. SpringDoc OpenAPI整合:使用@Operation、@ApiResponse註釋所有控制器,為公共和管理API建立分組文檔 2.為產品API實現HATEOAS:每個產品響應包含自鏈接、收藏鏈接和相關資源鏈接 3.為ProductController建立API版本控制:/api/v1和/api/v2,v2有附加欄位並更改了回應格式