Quarkus 開發模式
適用於雲端原生、事件驅動服務的 Quarkus 3.x 架構與 API 模式,並整合 Apache Camel。
啟動時機
- 使用 JAX-RS 或 RESTEasy Reactive 建構 REST API
- 組織 resource → service → repository 分層架構
- 使用 Apache Camel 和 RabbitMQ 實作事件驅動模式
- 設定 Hibernate Panache、快取或反應式串流
- 加入驗證、例外對映或分頁功能
- 為開發/測試/正式環境設定設定檔(YAML 設定)
- 使用 LogContext 和 Logback/Logstash encoder 進行自訂日誌
- 使用 CompletableFuture 處理非同步操作
- 實作條件式流程處理
- 使用 GraalVM 原生編譯
具有多重依賴的服務層
@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
public class OrderProcessingService {
private final OrderValidator orderValidator;
private final EventService eventService;
private final OrderRepository orderRepository;
private final FulfillmentPublisher fulfillmentPublisher;
private final AuditPublisher auditPublisher;
@Transactional
public OrderReceipt process(CreateOrderCommand command) {
ValidationResult validation = orderValidator.validate(command);
if (!validation.valid()) {
eventService.createErrorEvent(command, "ORDER_REJECTED", validation.message());
throw new WebApplicationException(validation.message(), Response.Status.BAD_REQUEST);
}
Order order = Order.from(command);
orderRepository.persist(order);
OrderReceipt receipt = OrderReceipt.from(order);
fulfillmentPublisher.publishAsync(receipt);
auditPublisher.publish("ORDER_ACCEPTED", receipt);
eventService.createSuccessEvent(receipt, "ORDER_ACCEPTED");
log.info("已處理訂單 {}", order.id);
return receipt;
}
}
關鍵模式:
@RequiredArgsConstructor:透過 Lombok 進行建構子注入@Slf4j:使用 Logback 進行日誌記錄@Transactional:在透過 Panache 或 repository 寫入的服務方法上使用- 在持久化或發佈訊息前驗證輸入
- 針對成功/錯誤情境進行事件追蹤
- 非同步 Camel 訊息發佈
自訂日誌內容模式(Logback)
@ApplicationScoped
public class ProcessingService {
public void processDocument(Document doc) {
LogContext logContext = CustomLog.getCurrentContext();
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
// 將內容新增至所有日誌陳述
logContext.put("documentId", doc.getId().toString());
logContext.put("documentType", doc.getType());
logContext.put("userId", SecurityContext.getUserId());
log.info("開始文件處理");
// 此範圍內的所有日誌都會繼承該內容
processInternal(doc);
log.info("文件處理完成");
} catch (Exception e) {
log.error("文件處理失敗", e);
throw e;
}
}
}
Logback 設定(logback.xml):
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>true</includeContext>
<includeMdc>true</includeMdc>
</encoder>
</appender>
<logger name="com.example" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
事件服務模式
@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
public class EventService {
private final EventRepository eventRepository;
private final ObjectMapper objectMapper;
public void createSuccessEvent(Object payload, String eventType) {
Objects.requireNonNull(payload, "酬載不可為 null");
Event event = new Event();
event.setType(eventType);
event.setStatus(EventStatus.SUCCESS);
event.setPayload(serializePayload(payload));
event.setTimestamp(Instant.now());
eventRepository.persist(event);
log.info("已建立成功事件:{}", eventType);
}
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
Objects.requireNonNull(payload, "酬載不可為 null");
if (errorMessage == null || errorMessage.isBlank()) {
throw new IllegalArgumentException("錯誤訊息不可為空白");
}
Event event = new Event();
event.setType(eventType);
event.setStatus(EventStatus.ERROR);
event.setErrorMessage(errorMessage);
event.setPayload(serializePayload(payload));
event.setTimestamp(Instant.now());
eventRepository.persist(event);
log.error("已建立錯誤事件:{} - {}", eventType, errorMessage);
}
private String serializePayload(Object payload) {
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
throw new IllegalStateException("序列化事件酬載失敗", e);
}
}
}
Camel 訊息發佈(RabbitMQ)
@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
public class BusinessRulesPublisher {
private final ProducerTemplate producerTemplate;
public void publishSync(BusinessRulesPayload payload) {
producerTemplate.sendBody(
"direct:business-rules-publisher",
payload
);
}
}
Camel 路由設定:
@ApplicationScoped
public class BusinessRulesRoute extends RouteBuilder {
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
String businessRulesQueue;
@ConfigProperty(name = "rabbitmq.host")
String rabbitHost;
@ConfigProperty(name = "rabbitmq.port")
Integer rabbitPort;
@Override
public void configure() {
from("direct:business-rules-publisher")
.routeId("business-rules-publisher")
.log("正在發佈訊息至 RabbitMQ:${body}")
.marshal().json(JsonLibrary.Jackson)
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
businessRulesQueue, rabbitHost, rabbitPort);
}
}
Camel 直接路由(記憶體內)
@ApplicationScoped
public class DocumentProcessingRoute extends RouteBuilder {
@Override
public void configure() {
// 錯誤處理
onException(ValidationException.class)
.handled(true)
.to("direct:validation-error-handler")
.log("驗證錯誤:${exception.message}");
// 主要處理路由
from("direct:process-document")
.routeId("document-processing")
.log("正在處理文件:${header.documentId}")
.bean(DocumentValidator.class, "validate")
.bean(DocumentTransformer.class, "transform")
.choice()
.when(header("documentType").isEqualTo("INVOICE"))
.to("direct:process-invoice")
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
.to("direct:process-credit-note")
.otherwise()
.to("direct:process-generic")
.end();
from("direct:validation-error-handler")
.bean(EventService.class, "createErrorEvent")
.log("已處理驗證錯誤");
}
}
Camel 檔案處理
@ApplicationScoped
public class FileMonitoringRoute extends RouteBuilder {
@ConfigProperty(name = "file.input.directory")
String inputDirectory;
@ConfigProperty(name = "file.processed.directory")
String processedDirectory;
@ConfigProperty(name = "file.error.directory")
String errorDirectory;
@Override
public void configure() {
from("file:" + inputDirectory + "?move=" + processedDirectory +
"&moveFailed=" + errorDirectory + "&delay=5000")
.routeId("file-monitor")
.log("正在處理檔案:${header.CamelFileName}")
.to("direct:process-file");
from("direct:process-file")
.bean(OrderProcessingService.class, "processFile")
.log("檔案處理完成");
}
}
Camel Bean 調用
@ApplicationScoped
public class InvoiceRoute extends RouteBuilder {
@Override
public void configure() {
from("direct:invoice-validation")
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
.log("驗證結果:${body}");
from("direct:persist-and-publish")
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
.bean(BusinessRulesPublisher.class, "publishAsync")
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
}
}
REST API 結構
@Path("/api/documents")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class DocumentResource {
private final DocumentService documentService;
@GET
public Response list(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
List<Document> documents = documentService.list(page, size);
return Response.ok(documents).build();
}
@POST
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
Document document = documentService.create(request);
URI location = uriInfo.getAbsolutePathBuilder()
.path(String.valueOf(document.id))
.build();
return Response.created(location).entity(DocumentResponse.from(document)).build();
}
@GET
@Path("/{id}")
public Response getById(@PathParam("id") Long id) {
return documentService.findById(id)
.map(DocumentResponse::from)
.map(Response::ok)
.orElse(Response.status(Response.Status.NOT_FOUND))
.build();
}
}
Repository 模式(Panache Repository)
@ApplicationScoped
public class DocumentRepository implements PanacheRepository<Document> {
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
return find("status = ?1 order by createdAt desc", status)
.page(page, size)
.list();
}
public Optional<Document> findByReferenceNumber(String referenceNumber) {
return find("referenceNumber", referenceNumber).firstResultOptional();
}
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
}
}
具有交易的服務層
@ApplicationScoped
@RequiredArgsConstructor
public class DocumentService {
private final DocumentRepository repo;
private final EventService eventService;
@Transactional
public Document create(CreateDocumentRequest request) {
Document document = new Document();
document.setReferenceNumber(request.referenceNumber());
document.setDescription(request.description());
document.setStatus(DocumentStatus.PENDING);
document.setCreatedAt(Instant.now());
repo.persist(document);
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
return document;
}
public Optional<Document> findById(Long id) {
return repo.findByIdOptional(id);
}
public List<Document> list(int page, int size) {
return repo.findAll()
.page(page, size)
.list();
}
}
DTO 與驗證
public record CreateDocumentRequest(
@NotBlank @Size(max = 200) String referenceNumber,
@NotBlank @Size(max = 2000) String description,
@NotNull @FutureOrPresent Instant validUntil,
@NotEmpty List<@NotBlank String> categories) {}
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
public static DocumentResponse from(Document document) {
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
document.getStatus());
}
}
例外對映
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
String message = exception.getConstraintViolations().stream()
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
.collect(Collectors.joining(", "));
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "validation_error", "message", message))
.build();
}
}
@Provider
@Slf4j
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
log.error(\