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

第 3 課:Quarkus REST — 建立專業的 RESTful API

Quarkus REST (Jakarta REST) 具有 @Path、@GET、@POST、JSON 序列化、CORS、OpenAPI 和 Swagger UI、API 版本控制。

💻 程式設計 — 第 2 課 第 3 課:Quarkus REST — 建立 RESTful API 專業的

Quarkus 微服務:從基礎知識到生產

第 1 部分:Quarkus 平台和專案設置

亞洲開發網

簡介

Quarkus REST(以前稱為 RESTEasy Reactive)是 Quarkus 中 Jakarta REST 的預設實作。它旨在根據返回類型處理 IO 線程(非阻塞)或 工作線程(阻塞)上的請求,提供高吞吐量,而無需編寫複雜的反應式程式碼。

Jakarta REST 基本註解

資源類

package com.xdev.ecommerce.product;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;

@Path("/api/v1/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

    @GET
    public List<ProductDTO> list(
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("size") @DefaultValue("20") int size,
            @QueryParam("category") String category) {
        // Trả về list → tự động serialize sang JSON
        return productService.findAll(page, size, category);
    }

    @GET
    @Path("/{id}")
    public ProductDTO getById(@PathParam("id") Long id) {
        return productService.findById(id);
    }

    @POST
    public Response create(CreateProductRequest request) {
        ProductDTO created = productService.create(request);
        return Response
                .created(URI.create("/api/v1/products/" + created.id()))
                .entity(created)
                .build();
    }

    @PUT
    @Path("/{id}")
    public ProductDTO update(@PathParam("id") Long id,
                             UpdateProductRequest request) {
        return productService.update(id, request);
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
        productService.delete(id);
        return Response.noContent().build();
    }
}

有 Java 記錄的 DTO

// Request DTOs
public record CreateProductRequest(
    String name,
    String description,
    BigDecimal price,
    Long categoryId,
    int stockQuantity
) {}

public record UpdateProductRequest(
    String name,
    String description,
    BigDecimal price,
    int stockQuantity
) {}

// Response DTO
public record ProductDTO(
    Long id,
    String name,
    String description,
    BigDecimal price,
    String categoryName,
    int stockQuantity,
    LocalDateTime createdAt
) {}

Jackson JSON 序列化

傑克遜配置

# application.properties
quarkus.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss
quarkus.jackson.timezone=Asia/Ho_Chi_Minh
quarkus.jackson.serialization.indent-output=true
quarkus.jackson.serialization.write-dates-as-timestamps=false

自訂物件映射器

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;

@Singleton
public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {

    @Override
    public void customize(ObjectMapper mapper) {
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

請求/回應過濾

伺服器過濾器

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import io.vertx.core.http.HttpServerRequest;

public class LoggingFilter {

    @ServerRequestFilter
    public void logRequest(ContainerRequestContext ctx,
                           HttpServerRequest request) {
        // Log incoming request
        String method = ctx.getMethod();
        String path = ctx.getUriInfo().getPath();
        String clientIp = request.remoteAddress().host();
        Log.infof("→ %s %s from %s", method, path, clientIp);
    }

    @ServerResponseFilter
    public void logResponse(ContainerResponseContext ctx) {
        int status = ctx.getStatus();
        Log.infof("← Response: %d", status);
    }
}

自訂標頭篩選器

import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import jakarta.ws.rs.container.ContainerResponseContext;

public class SecurityHeadersFilter {

    @ServerResponseFilter
    public void addSecurityHeaders(ContainerResponseContext response) {
        response.getHeaders().putSingle(
            "X-Content-Type-Options", "nosniff");
        response.getHeaders().putSingle(
            "X-Frame-Options", "DENY");
        response.getHeaders().putSingle(
            "Cache-Control", "no-store");
    }
}

CORS 配置

# application.properties
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000,https://ecommerce.xdev.asia
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=Content-Type,Authorization
quarkus.http.cors.exposed-headers=X-Total-Count
quarkus.http.cors.access-control-max-age=24H

分頁回應模式

import jakarta.ws.rs.core.Response;

@GET
public Response list(@QueryParam("page") @DefaultValue("0") int page,
                     @QueryParam("size") @DefaultValue("20") int size) {

    PanacheQuery<Product> query = Product.findAll();
    List<ProductDTO> items = query
            .page(Page.of(page, size))
            .list()
            .stream()
            .map(ProductDTO::from)
            .toList();

    long total = query.count();

    return Response.ok(items)
            .header("X-Total-Count", total)
            .header("X-Page", page)
            .header("X-Page-Size", size)
            .header("X-Total-Pages", (total + size - 1) / size)
            .build();
}

OpenAPI 和 Swagger UI

# Swagger UI tự động có sẵn ở dev mode
# http://localhost:8080/q/swagger-ui

# Tùy chỉnh OpenAPI info
quarkus.smallrye-openapi.info-title=Product Service API
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=API quản lý sản phẩm E-Commerce
quarkus.smallrye-openapi.info-contact-name=xdev.asia
quarkus.smallrye-openapi.info-contact-url=https://blog.xdev.asia

# Cho phép Swagger UI ở production (tùy chọn)
quarkus.swagger-ui.always-include=true

OpenAPI 註釋

import org.eclipse.microprofile.openapi.annotations.*;
import org.eclipse.microprofile.openapi.annotations.media.*;
import org.eclipse.microprofile.openapi.annotations.parameters.*;
import org.eclipse.microprofile.openapi.annotations.responses.*;

@Path("/api/v1/products")
@Tag(name = "Products", description = "Quản lý sản phẩm")
public class ProductResource {

    @GET
    @Operation(summary = "Danh sách sản phẩm",
               description = "Lấy danh sách sản phẩm với phân trang")
    @APIResponse(responseCode = "200",
                 description = "Thành công",
                 content = @Content(
                     mediaType = "application/json",
                     schema = @Schema(
                         implementation = ProductDTO.class)))
    public List<ProductDTO> list(
            @Parameter(description = "Trang (bắt đầu từ 0)")
            @QueryParam("page") @DefaultValue("0") int page,
            @Parameter(description = "Số lượng mỗi trang")
            @QueryParam("size") @DefaultValue("20") int size) {
        // ...
    }

    @POST
    @Operation(summary = "Tạo sản phẩm mới")
    @APIResponse(responseCode = "201",
                 description = "Sản phẩm được tạo thành công")
    @APIResponse(responseCode = "400",
                 description = "Dữ liệu không hợp lệ")
    public Response create(
            @RequestBody(required = true,
                content = @Content(schema = @Schema(
                    implementation = CreateProductRequest.class)))
            CreateProductRequest request) {
        // ...
    }
}

API 版本控制策略

URI 版本控制(建議用於微服務)

@Path("/api/v1/products")
public class ProductResourceV1 {
    @GET
    public List<ProductDTOv1> list() { /* ... */ }
}

@Path("/api/v2/products")
public class ProductResourceV2 {
    @GET
    public List<ProductDTOv2> list() { /* ... */ }
}

標頭版本控制

@Path("/api/products")
public class ProductResource {

    @GET
    public Response list(@HeaderParam("X-API-Version") 
                         @DefaultValue("1") int version) {
        return switch (version) {
            case 2 -> Response.ok(listV2()).build();
            default -> Response.ok(listV1()).build();
        };
    }
}

練習

  1. 創建 ProductResource 具有完整的 CRUD 操作
  2. 使用 Java 記錄進行請求/回應 DTO 3.添加帶標題的分頁支持 X-Total-Count, X-Page
  3. 配置 CORS http://localhost:3000
  4. 存取 Swagger UI 並測試所有端點
  5. 新增 OpenAPI 註釋來描述每個端點的詳細信息

總結

  • Quarkus REST 使用 Jakarta REST 註解(@Path, @GET, @POST...)
  • Java Records 適用於不可變的 DTO
  • 伺服器過濾器 (@ServerRequestFilter, @ServerResponseFilter)用於跨領域關注
  • CORS 配置於 application.properties
  • OpenAPI + Swagger UI 自動產生文檔
  • API 版本控制 透過 URI (/api/v1/, /api/v2/)簡單明了

下一篇文章:PostgreSQL 和 Hibernate ORM Panache — 有效的資料層。