Quarkus Patterns

作者 affaan-mef648e01899b無授權條款275K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 天前更新

Patrones de arquitectura Quarkus 3.x LTS con Camel para mensajería, diseño de API RESTful, servicios CDI, acceso a datos con Panache y procesamiento asíncrono.

AI 產生的概覽

Quarkus 3.x 架構模式,涵蓋 REST API、CDI 服務、Panache 資料存取、Camel 訊息與非同步處理。

功能
提供建構 Quarkus 3.x 雲原生服務的參考模式與程式碼範例,涵蓋 resource-service-repository 分層、JAX-RS/RESTEasy 端點、DTO 驗證與例外對應。也記錄了用於 RabbitMQ 與檔案處理的 Apache Camel 路由、Panache 儲存庫、交易、快取、CompletableFuture 非同步作業、結構化 Logback 日誌、健康檢查以及 YAML 多環境設定。產出的是指引與程式碼片段,而非產生的檔案。
適用情境
在建立或檢視 Quarkus 服務時,若需要 REST 分層、CDI 服務、Panache 持久化、Camel 訊息或非同步作業方面的成熟模式,可使用本技能。設定多環境、日誌或健康檢查時同樣適用。
執行需求
本技能不附帶指令碼,僅包含說明與程式碼範例。套用這些範例需要 Quarkus 3.x 專案、Java 17、Maven、Lombok、Hibernate Panache、Apache Camel(含 spring-rabbitmq 元件)、搭配 Logstash 編碼器的 Logback,以及選用的資料庫、RabbitMQ 代理與相容 S3 的儲存空間。

Patrones de Desarrollo Quarkus

Patrones de arquitectura y API de Quarkus 3.x para servicios cloud-native y orientados a eventos con Apache Camel.

Cuándo Activar

  • Construir APIs REST con JAX-RS o RESTEasy Reactive
  • Estructurar capas resource → service → repository
  • Implementar patrones orientados a eventos con Apache Camel y RabbitMQ
  • Configurar Hibernate Panache, caché o streams reactivos
  • Agregar validación, mapeo de excepciones o paginación
  • Configurar perfiles para entornos dev/staging/producción (configuración YAML)
  • Logging personalizado con LogContext y Logback/Logstash encoder
  • Trabajar con CompletableFuture para operaciones asíncronas
  • Implementar procesamiento condicional de flujos
  • Trabajar con compilación nativa GraalVM

Capa de Servicio con Múltiples Dependencias

java
@Slf4j@ApplicationScoped@RequiredArgsConstructorpublic 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("Orden procesada {}", order.id);        return receipt;    }}

Patrones Clave:

  • @RequiredArgsConstructor para inyección por constructor mediante Lombok
  • @Slf4j para logging con Logback
  • @Transactional en métodos de servicio que escriben a través de Panache o repositorios
  • Validar entrada antes de persistencia o publicación de mensajes
  • Seguimiento de eventos para escenarios de éxito/error
  • Publicación asíncrona de mensajes Camel

Patrón de Contexto de Logging Personalizado (Logback)

java
@ApplicationScopedpublic 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("Iniciando procesamiento de documento");
            processInternal(doc);
            log.info("Procesamiento de documento completado");        } catch (Exception e) {            log.error("Error en el procesamiento de documento", e);            throw e;        }    }}

Configuración de Logback (logback.xml):

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>

Patrón de Servicio de Eventos

java
@Slf4j@ApplicationScoped@RequiredArgsConstructorpublic class EventService {    private final EventRepository eventRepository;    private final ObjectMapper objectMapper;
    public void createSuccessEvent(Object payload, String eventType) {        Objects.requireNonNull(payload, "El payload no puede ser 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("Evento de éxito creado: {}", eventType);    }
    public void createErrorEvent(Object payload, String eventType, String errorMessage) {        Objects.requireNonNull(payload, "El payload no puede ser null");        if (errorMessage == null || errorMessage.isBlank()) {            throw new IllegalArgumentException("El mensaje de error no puede estar en blanco");        }        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("Evento de error creado: {} - {}", eventType, errorMessage);    }
    private String serializePayload(Object payload) {        try {            return objectMapper.writeValueAsString(payload);        } catch (JsonProcessingException e) {            throw new IllegalStateException("Error al serializar el payload del evento", e);        }    }}

Publicación de Mensajes Camel (RabbitMQ)

java
@Slf4j@ApplicationScoped@RequiredArgsConstructorpublic class BusinessRulesPublisher {    private final ProducerTemplate producerTemplate;
    public void publishSync(BusinessRulesPayload payload) {        producerTemplate.sendBody(            "direct:business-rules-publisher",            payload        );    }}

Configuración de Ruta Camel:

java
@ApplicationScopedpublic 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("Publicando mensaje en RabbitMQ: ${body}")            .marshal().json(JsonLibrary.Jackson)            .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",                businessRulesQueue, rabbitHost, rabbitPort);    }}

Rutas Camel Direct (En Memoria)

java
@ApplicationScopedpublic class DocumentProcessingRoute extends RouteBuilder {
    @Override    public void configure() {        onException(ValidationException.class)            .handled(true)            .to("direct:validation-error-handler")            .log("Error de validación: ${exception.message}");
        from("direct:process-document")            .routeId("document-processing")            .log("Procesando documento: ${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();    }}

Procesamiento de Archivos Camel

java
@ApplicationScopedpublic 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("Procesando archivo: ${header.CamelFileName}")            .to("direct:process-file");    }}

Estructura de API REST

java
@Path("/api/documents")@Produces(MediaType.APPLICATION_JSON)@Consumes(MediaType.APPLICATION_JSON)@RequiredArgsConstructorpublic 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();  }}

Patrón de Repositorio (Panache Repository)

java
@ApplicationScopedpublic 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();  }}

Capa de Servicio con Transacciones

java
@ApplicationScoped@RequiredArgsConstructorpublic 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;  }}

DTOs y Validación

java
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());  }}

Mapeo de Excepciones

java
@Providerpublic 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();  }}

Operaciones Asíncronas con CompletableFuture

java
@Slf4j@ApplicationScoped@RequiredArgsConstructorpublic class FileStorageService {    private final S3Client s3Client;    private final ExecutorService executorService;
    public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(            InputStream inputStream,            long size,            LogContext logContext,            InvoiceFormat format) {
        return CompletableFuture.supplyAsync(() -> {            try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {                String path = generateStoragePath(format);
                PutObjectRequest request = PutObjectRequest.builder()                    .bucket(bucketName)                    .key(path)                    .contentLength(size)                    .build();
                s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
                return new StoredDocumentInfo(path, size, Instant.now());            } catch (Exception e) {                log.error("Error al subir archivo a S3", e);                throw new StorageException("Subida fallida", e);            }        }, executorService);    }}

Caché

java
@ApplicationScoped@RequiredArgsConstructorpublic class DocumentCacheService {  private final DocumentRepository repo;
  @CacheResult(cacheName = "document-cache")  public Optional<Document> getById(@CacheKey Long id) {    return repo.findByIdOptional(id);  }
  @CacheInvalidate(cacheName = "document-cache")  public void evict(@CacheKey Long id) {}}

Configuración como YAML

yaml
# application.yml"%dev":  quarkus:    datasource:      jdbc:        url: jdbc:postgresql://localhost:5432/dev_db      username: dev_user      password: ${DB_PASSWORD}
"%test":  quarkus:    datasource:      jdbc:        url: jdbc:h2:mem:test    hibernate-orm:      database:        generation: drop-and-create
"%prod":  quarkus:    datasource:      jdbc:        url: ${DATABASE_URL}      username: ${DB_USER}      password: ${DB_PASSWORD}

Health Checks

java
@Readiness@ApplicationScoped@RequiredArgsConstructorpublic class DatabaseHealthCheck implements HealthCheck {  private final AgroalDataSource dataSource;
  @Override  public HealthCheckResponse call() {    try (Connection conn = dataSource.getConnection()) {      boolean valid = conn.isValid(2);      return HealthCheckResponse.named("Database connection")          .status(valid)          .build();    } catch (SQLException e) {      return HealthCheckResponse.down("Database connection");    }  }}

Dependencias (Maven)

xml
<properties>    <quarkus.platform.version>3.27.0</quarkus.platform.version>    <lombok.version>1.18.42</lombok.version>    <assertj-core.version>3.24.2</assertj-core.version>    <jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>    <maven.compiler.release>17</maven.compiler.release></properties>

Buenas Prácticas

Arquitectura

  • Usar @RequiredArgsConstructor con Lombok para inyección por constructor
  • Mantener la capa de servicio delgada; delegar lógica compleja a clases especializadas
  • Usar rutas Camel para enrutamiento de mensajes y patrones de integración
  • Preferir el patrón Panache Repository para acceso a datos

Orientado a Eventos

  • Siempre rastrear operaciones con EventService (eventos de éxito/error)
  • Usar endpoints direct: de Camel para enrutamiento en memoria
  • Usar el componente spring-rabbitmq para integración con RabbitMQ

Logging

  • Usar Logback con Logstash encoder para logging estructurado
  • Propagar LogContext a través de llamadas de servicio con SafeAutoCloseable
  • Usar @Slf4j en lugar de instanciación manual del logger

Configuración

  • Usar configuración YAML (quarkus-config-yaml)
  • Configuración consciente de perfil para entornos dev/test/prod
  • Externalizar configuración sensible a variables de entorno

來源與署名

來源:affaan-m/ecc位於docs/es/skills/quarkus-patterns提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架