簡介
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();
};
}
}
練習
- 創建
ProductResource具有完整的 CRUD 操作 - 使用 Java 記錄進行請求/回應 DTO
3.添加帶標題的分頁支持
X-Total-Count,X-Page - 配置 CORS
http://localhost:3000 - 存取 Swagger UI 並測試所有端點
- 新增 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 — 有效的資料層。