From 5df575bff87892aedbe602f5c3410794895b4e4d Mon Sep 17 00:00:00 2001 From: AlexisLeDain Date: Tue, 12 May 2026 15:05:16 +0200 Subject: [PATCH] docs: drop incomplete ja-JP and zh-CN Quarkus translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ja-JP and zh-CN translations of the four Quarkus skills are missing content (quarkus-tdd: 8/11 sections, quarkus-verification: 10/15 sections; quarkus-security trimmed by 60-100 lines). Removing rather than shipping partial translations. Turkish translations remain — they mirror the English source. ja-JP and zh-CN to be redone in a focused follow-up. --- docs/ja-JP/skills/quarkus-patterns/SKILL.md | 766 ------------------ docs/ja-JP/skills/quarkus-security/SKILL.md | 404 --------- docs/ja-JP/skills/quarkus-tdd/SKILL.md | 390 --------- .../skills/quarkus-verification/SKILL.md | 312 ------- docs/zh-CN/skills/quarkus-patterns/SKILL.md | 740 ----------------- docs/zh-CN/skills/quarkus-security/SKILL.md | 367 --------- docs/zh-CN/skills/quarkus-tdd/SKILL.md | 389 --------- .../skills/quarkus-verification/SKILL.md | 312 ------- 8 files changed, 3680 deletions(-) delete mode 100644 docs/ja-JP/skills/quarkus-patterns/SKILL.md delete mode 100644 docs/ja-JP/skills/quarkus-security/SKILL.md delete mode 100644 docs/ja-JP/skills/quarkus-tdd/SKILL.md delete mode 100644 docs/ja-JP/skills/quarkus-verification/SKILL.md delete mode 100644 docs/zh-CN/skills/quarkus-patterns/SKILL.md delete mode 100644 docs/zh-CN/skills/quarkus-security/SKILL.md delete mode 100644 docs/zh-CN/skills/quarkus-tdd/SKILL.md delete mode 100644 docs/zh-CN/skills/quarkus-verification/SKILL.md diff --git a/docs/ja-JP/skills/quarkus-patterns/SKILL.md b/docs/ja-JP/skills/quarkus-patterns/SKILL.md deleted file mode 100644 index 0c3664fe..00000000 --- a/docs/ja-JP/skills/quarkus-patterns/SKILL.md +++ /dev/null @@ -1,766 +0,0 @@ ---- -name: quarkus-patterns -description: Quarkus 3.x LTSアーキテクチャパターン、Camelメッセージング、RESTful API設計、CDIサービス、Panacheデータアクセス、非同期処理。イベント駆動アーキテクチャを持つJava Quarkusバックエンド作業に使用。 -origin: ECC ---- - -# Quarkus 開発パターン - -Apache Camelを使用したクラウドネイティブなイベント駆動サービスのためのQuarkus 3.xアーキテクチャとAPIパターン。 - -## いつアクティブにするか - -- JAX-RSまたはRESTEasy ReactiveでREST APIを構築する -- リソース → サービス → リポジトリレイヤーを構造化する -- Apache CamelとRabbitMQでイベント駆動パターンを実装する -- Hibernate Panache、キャッシング、またはリアクティブストリームを構成する -- バリデーション、例外マッピング、またはページネーションを追加する -- dev/staging/production環境のプロファイルを設定する(YAML構成) -- LogContextとLogback/Logstashエンコーダーでカスタムロギング -- CompletableFutureで非同期操作を行う -- 条件付きフロー処理を実装する -- GraalVMネイティブコンパイルで作業する - -## 複数依存関係を持つサービスレイヤー(Lombok) - -```java -@Slf4j -@ApplicationScoped -@RequiredArgsConstructor -public class As2ProcessingService { - - private final InvoiceFlowValidator invoiceFlowValidator; - private final EventService eventService; - private final DocumentJobService documentJobService; - private final BusinessRulesPublisher businessRulesPublisher; - private final FileStorageService fileStorageService; - - public void processFile(Path filePath) throws Exception { - LogContext logContext = CustomLog.getCurrentContext(); - try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { - - String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID); - - // 条件付きフローロジック - boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW)); - log.info("Is CHORUS_FLOW message: {}", isChorusFlow); - - ValidationFlowConfig validationFlowConfig = isChorusFlow - ? ValidationFlowConfig.xsdOnly() - : ValidationFlowConfig.allValidations(); - - InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator - .validateFlowWithConfig(filePath, validationFlowConfig, - EInvoiceSyntaxFormat.UBL, logContext); - - FlowProfile flowProfile = isChorusFlow ? - FlowProfile.EXTENDED_CTC_FR : - this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult, - invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile()); - - log.info("Invoice validation completed. Message is valid"); - - // CompletableFuture非同期操作 - try(InputStream inputStream = Files.newInputStream(filePath)) { - CompletableFuture documentInfoCompletableFuture = - fileStorageService.uploadOriginalFile(inputStream, - invoiceValidationResult.getSize(), logContext, - invoiceValidationResult.getInvoiceFormat()); - - StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join(); - log.info("File uploaded successfully: {}", documentInfo.getPath()); - - if (StringUtils.isBlank(documentInfo.getPath())) { - String errorMsg = "File path is empty after upload"; - log.error(errorMsg); - this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg); - throw new As2ServerProcessingException(errorMsg); - } - - this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE"); - - BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities( - documentInfo, originalFileName, structureIdPartner, - flowProfile, invoiceValidationResult.getDocumentHash()); - - // 非同期Camelパブリッシング - businessRulesPublisher.publishAsync(payload); - this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT"); - } - } - } -} -``` - -**主要パターン:** -- Lombokによるコンストラクタインジェクション用の`@RequiredArgsConstructor` -- Logbackロギング用の`@Slf4j` -- try-with-resourcesによるスコープ付きLogContext -- ランタイムパラメータに基づく条件付きフローロジック -- 非同期操作用の`.join()`付きCompletableFuture -- 成功/エラーシナリオのイベントトラッキング -- 非同期Camelメッセージパブリッシング - -## カスタムロギングコンテキストパターン(Logback) - -```java -@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("Starting document processing"); - - // このスコープ内のすべてのログはコンテキストを継承 - processInternal(doc); - - log.info("Document processing completed"); - } catch (Exception e) { - log.error("Document processing failed", e); - throw e; - } - } -} -``` - -**Logback構成(logback.xml):** - -```xml - - - - true - true - - - - - - - - -``` - -## イベントサービスパターン - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class EventService { - private final EventRepository eventRepository; - private final ObjectMapper objectMapper; - - public void createSuccessEvent(Object payload, String eventType) { - Objects.requireNonNull(payload, "Payload cannot be 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("Success event created: {}", eventType); - } - - public void createErrorEvent(Object payload, String eventType, String errorMessage) { - Objects.requireNonNull(payload, "Payload cannot be null"); - if (errorMessage == null || errorMessage.isBlank()) { - throw new IllegalArgumentException("Error message cannot be blank"); - } - 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("Error event created: {} - {}", eventType, errorMessage); - } - - private String serializePayload(Object payload) { - try { - return objectMapper.writeValueAsString(payload); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Failed to serialize event payload", e); - } - } -} -``` - -## Camelメッセージパブリッシング(RabbitMQ) - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class BusinessRulesPublisher { - private final ProducerTemplate producerTemplate; - - @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") - String businessRulesQueue; - - public void publishAsync(BusinessRulesPayload payload) { - producerTemplate.asyncSendBody( - "direct:business-rules-publisher", - payload - ); - log.info("Message published to business rules queue: {}", payload.getDocumentId()); - } - - public void publishSync(BusinessRulesPayload payload) { - producerTemplate.sendBody( - "direct:business-rules-publisher", - payload - ); - } -} -``` - -**Camelルート構成:** - -```java -@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("Publishing message to RabbitMQ: ${body}") - .marshal().json(JsonLibrary.Jackson) - .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", - businessRulesQueue, rabbitHost, rabbitPort); - } -} -``` - -## Camel Directルート(インメモリ) - -```java -@ApplicationScoped -public class DocumentProcessingRoute extends RouteBuilder { - - @Override - public void configure() { - // エラーハンドリング - onException(ValidationException.class) - .handled(true) - .to("direct:validation-error-handler") - .log("Validation error: ${exception.message}"); - - // メイン処理ルート - from("direct:process-document") - .routeId("document-processing") - .log("Processing document: ${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("Validation error handled"); - } -} -``` - -## Camelファイル処理 - -```java -@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("Processing file: ${header.CamelFileName}") - .to("direct:process-file"); - - from("direct:process-file") - .bean(As2ProcessingService.class, "processFile") - .log("File processing completed"); - } -} -``` - -## Camel Bean呼び出し - -```java -@ApplicationScoped -public class InvoiceRoute extends RouteBuilder { - - @Override - public void configure() { - from("direct:invoice-validation") - .bean(InvoiceFlowValidator.class, "validateFlowWithConfig") - .log("Validation result: ${body}"); - - from("direct:persist-and-publish") - .bean(DocumentJobService.class, "createDocumentAndJobEntities") - .bean(BusinessRulesPublisher.class, "publishAsync") - .bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')"); - } -} -``` - -## REST API構造 - -```java -@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 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(); - } -} -``` - -## リポジトリパターン(Panache Repository) - -```java -@ApplicationScoped -public class DocumentRepository implements PanacheRepository { - - public List findByStatus(DocumentStatus status, int page, int size) { - return find("status = ?1 order by createdAt desc", status) - .page(page, size) - .list(); - } - - public Optional 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()); - } -} -``` - -## トランザクション付きサービスレイヤー - -```java -@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 findById(Long id) { - return repo.findByIdOptional(id); - } - - public List list(int page, int size) { - return repo.findAll() - .page(page, size) - .list(); - } -} -``` - -## DTOとバリデーション - -```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()); - } -} -``` - -## 例外マッピング - -```java -@Provider -public class ValidationExceptionMapper implements ExceptionMapper { - @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 { - - @Override - public Response toResponse(Exception exception) { - log.error("Unhandled exception", exception); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "internal_error", "message", "An unexpected error occurred")) - .build(); - } -} -``` - -## CompletableFuture非同期操作 - -```java -@Slf4j -@ApplicationScoped -@RequiredArgsConstructor -public class FileStorageService { - private final S3Client s3Client; - private final ExecutorService executorService; - - @ConfigProperty(name = "storage.bucket-name") String bucketName; - - public CompletableFuture 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)); - - log.info("File uploaded to S3: {}", path); - - return new StoredDocumentInfo(path, size, Instant.now()); - } catch (Exception e) { - log.error("Failed to upload file to S3", e); - throw new StorageException("Upload failed", e); - } - }, executorService); - } -} -``` - -## キャッシング - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class DocumentCacheService { - private final DocumentRepository repo; - - @CacheResult(cacheName = "document-cache") - public Optional getById(@CacheKey Long id) { - return repo.findByIdOptional(id); - } - - @CacheInvalidate(cacheName = "document-cache") - public void evict(@CacheKey Long id) {} - - @CacheInvalidateAll(cacheName = "document-cache") - public void evictAll() {} -} -``` - -## YAML構成 - -```yaml -# application.yml -"%dev": - quarkus: - datasource: - jdbc: - url: jdbc:postgresql://localhost:5432/dev_db - username: dev_user - password: dev_pass - hibernate-orm: - database: - generation: drop-and-create - - rabbitmq: - host: localhost - port: 5672 - username: guest - password: guest - -"%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} - hibernate-orm: - database: - generation: validate - - rabbitmq: - host: ${RABBITMQ_HOST} - port: ${RABBITMQ_PORT} - username: ${RABBITMQ_USER} - password: ${RABBITMQ_PASSWORD} - -# Camel構成 -camel: - rabbitmq: - queue: - business-rules: business-rules-queue - invoice-processing: invoice-processing-queue -``` - -## ヘルスチェック - -```java -@Readiness -@ApplicationScoped -@RequiredArgsConstructor -public 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"); - } - } -} - -@Liveness -@ApplicationScoped -public class CamelHealthCheck implements HealthCheck { - @Inject - CamelContext camelContext; - - @Override - public HealthCheckResponse call() { - boolean isStarted = camelContext.getStatus().isStarted(); - return HealthCheckResponse.named("Camel Context") - .status(isStarted) - .build(); - } -} -``` - -## 依存関係(Maven) - -```xml - - 3.27.0 - 1.18.42 - 3.24.2 - 0.8.13 - 17 - - - - - - io.quarkus.platform - quarkus-bom - ${quarkus.platform.version} - pom - import - - - io.quarkus.platform - quarkus-camel-bom - ${quarkus.platform.version} - pom - import - - - - - - - - io.quarkus - quarkus-arc - - - io.quarkus - quarkus-config-yaml - - - - - org.apache.camel.quarkus - camel-quarkus-spring-rabbitmq - - - org.apache.camel.quarkus - camel-quarkus-direct - - - org.apache.camel.quarkus - camel-quarkus-bean - - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - io.quarkiverse.logging.logback - quarkus-logging-logback - - - net.logstash.logback - logstash-logback-encoder - - -``` - -## ベストプラクティス - -### アーキテクチャ -- コンストラクタインジェクション用にLombokの`@RequiredArgsConstructor`を使用 -- サービスレイヤーは薄く保ち、複雑なロジックは専門クラスに委譲 -- メッセージルーティングと統合パターンにCamelルートを使用 -- データアクセスにはPanache Repositoryパターンを優先 - -### イベント駆動 -- 常にEventServiceで操作をトラッキング(成功/エラーイベント) -- インメモリルーティングにCamelの`direct:`エンドポイントを使用 -- RabbitMQ統合に`spring-rabbitmq`コンポーネントを使用 -- `ProducerTemplate.asyncSendBody()`で非同期パブリッシングを実装 - -### ロギング -- 構造化ロギング用にLogstashエンコーダー付きLogbackを使用 -- `SafeAutoCloseable`でサービスコール間でLogContextを伝播 -- リクエストトレーシング用にLogContextにコンテキスト情報を追加 -- 手動ロガーインスタンス化の代わりに`@Slf4j`を使用 - -### 非同期操作 -- ノンブロッキングI/O操作にCompletableFutureを使用 -- 完了を待つ必要がある場合は`.join()`を呼び出す -- CompletableFutureからの例外を適切にハンドリング -- トレーシング用に非同期操作にLogContextを渡す - -### 構成 -- YAML構成を使用(`quarkus-config-yaml`) -- dev/test/prod環境のプロファイル対応構成 -- 機密構成を環境変数に外部化 -- 型安全な構成インジェクション用に`@ConfigProperty`を使用 - -### バリデーション -- リソースレイヤーで`@Valid`によるバリデーション -- DTOにBean Validationアノテーションを使用 -- `@Provider`で例外を適切なHTTPレスポンスにマッピング - -### トランザクション -- データを変更するサービスメソッドに`@Transactional`を使用 -- トランザクションは短く焦点を絞る -- トランザクション内で非同期操作を呼び出さない - -### テスト -- ルートテストに`camel-quarkus-junit5`を使用 -- アサーションにAssertJを使用 -- すべての外部依存関係をモック -- 条件付きフローロジックを徹底的にテスト - -### Quarkus固有 -- 最新のLTSバージョン(3.x)を維持 -- ホットリロード用にQuarkus devモードを使用 -- 本番準備のためにヘルスチェックを追加 -- ネイティブコンパイル互換性を定期的にテスト diff --git a/docs/ja-JP/skills/quarkus-security/SKILL.md b/docs/ja-JP/skills/quarkus-security/SKILL.md deleted file mode 100644 index 69da4a28..00000000 --- a/docs/ja-JP/skills/quarkus-security/SKILL.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -name: quarkus-security -description: Quarkusセキュリティのベストプラクティス:認証、認可、JWT/OIDC、RBAC、入力バリデーション、CSRF、シークレット管理、依存関係セキュリティ。 -origin: ECC ---- - -# Quarkus セキュリティレビュー - -認証、認可、入力バリデーションによるQuarkusアプリケーションのセキュリティベストプラクティス。 - -## いつアクティブにするか - -- 認証の追加(JWT、OIDC、Basic Auth) -- @RolesAllowedまたはSecurityIdentityによる認可の実装 -- ユーザー入力のバリデーション(Bean Validation、カスタムバリデータ) -- CORSまたはセキュリティヘッダーの構成 -- シークレット管理(Vault、環境変数、構成ソース) -- レート制限またはブルートフォース保護の追加 -- 依存関係のCVEスキャン -- MicroProfile JWTまたはSmallRye JWTの使用 - -## 認証 - -### JWT認証 - -```java -@Path("/api/protected") -@Authenticated -public class ProtectedResource { - - @Inject - JsonWebToken jwt; - - @Inject - SecurityIdentity securityIdentity; - - @GET - public Response getData() { - String username = jwt.getName(); - Set roles = jwt.getGroups(); - return Response.ok(Map.of( - "username", username, - "roles", roles, - "principal", securityIdentity.getPrincipal().getName() - )).build(); - } -} -``` - -構成(application.properties): -```properties -mp.jwt.verify.publickey.location=publicKey.pem -mp.jwt.verify.issuer=https://auth.example.com - -# OIDC -quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm -quarkus.oidc.client-id=backend-service -quarkus.oidc.credentials.secret=${OIDC_SECRET} -``` - -### カスタム認証フィルター - -```java -@Provider -@Priority(Priorities.AUTHENTICATION) -public class CustomAuthFilter implements ContainerRequestFilter { - - @Inject - SecurityIdentity identity; - - @Override - public void filter(ContainerRequestContext requestContext) { - String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - - // ヘッダーが存在しないか不正な場合は即座に拒否 - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - return; - } - - String token = authHeader.substring(7); - if (!validateToken(token)) { - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - } - } - - private boolean validateToken(String token) { - // トークンバリデーションロジック - return true; - } -} -``` - -## 認可 - -### ロールベースアクセス制御 - -```java -@Path("/api/admin") -@RolesAllowed("ADMIN") -public class AdminResource { - - @GET - @Path("/users") - public List listUsers() { - return userService.findAll(); - } - - @DELETE - @Path("/users/{id}") - @RolesAllowed({"ADMIN", "SUPER_ADMIN"}) - public Response deleteUser(@PathParam("id") Long id) { - userService.delete(id); - return Response.noContent().build(); - } -} - -@Path("/api/users") -public class UserResource { - - @Inject - SecurityIdentity securityIdentity; - - @GET - @Path("/{id}") - @RolesAllowed("USER") - public Response getUser(@PathParam("id") Long id) { - // オーナーシップチェック - if (!securityIdentity.hasRole("ADMIN") && - !isOwner(id, securityIdentity.getPrincipal().getName())) { - return Response.status(Response.Status.FORBIDDEN).build(); - } - return Response.ok(userService.findById(id)).build(); - } - - private boolean isOwner(Long userId, String username) { - return userService.isOwner(userId, username); - } -} -``` - -### プログラマティックセキュリティ - -```java -@ApplicationScoped -public class SecurityService { - - @Inject - SecurityIdentity securityIdentity; - - public boolean canAccessResource(Long resourceId) { - if (securityIdentity.isAnonymous()) { - return false; - } - - if (securityIdentity.hasRole("ADMIN")) { - return true; - } - - String userId = securityIdentity.getPrincipal().getName(); - return resourceRepository.isOwner(resourceId, userId); - } -} -``` - -## 入力バリデーション - -### Bean Validation - -```java -// BAD: バリデーションなし -@POST -public Response createUser(UserDto dto) { - return Response.ok(userService.create(dto)).build(); -} - -// GOOD: バリデーション付きDTO -public record CreateUserDto( - @NotBlank @Size(max = 100) String name, - @NotBlank @Email String email, - @NotNull @Min(18) @Max(150) Integer age, - @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone -) {} - -@POST -@Path("/users") -public Response createUser(@Valid CreateUserDto dto) { - User user = userService.create(dto); - return Response.status(Response.Status.CREATED).entity(user).build(); -} -``` - -### カスタムバリデータ - -```java -@Target({ElementType.FIELD, ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = UsernameValidator.class) -public @interface ValidUsername { - String message() default "Invalid username format"; - Class[] groups() default {}; - Class[] payload() default {}; -} - -public class UsernameValidator implements ConstraintValidator { - @Override - public boolean isValid(String value, ConstraintValidatorContext context) { - if (value == null) return false; - return value.matches("^[a-zA-Z0-9_-]{3,20}$"); - } -} -``` - -## SQLインジェクション防止 - -### Panache Active Record(デフォルトで安全) - -```java -// GOOD: Panacheによるパラメータ化クエリ -List users = User.list("email = ?1 and active = ?2", email, true); - -Optional user = User.find("username", username).firstResultOptional(); - -// GOOD: 名前付きパラメータ -List users = User.list("email = :email and age > :minAge", - Parameters.with("email", email).and("minAge", 18)); -``` - -### ネイティブクエリ(パラメータを使用) - -```java -// BAD: 文字列連結 -@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true) - -// GOOD: パラメータ化ネイティブクエリ -@Entity -public class User extends PanacheEntity { - public static List findByEmailNative(String email) { - return getEntityManager() - .createNativeQuery("SELECT * FROM users WHERE email = :email", User.class) - .setParameter("email", email) - .getResultList(); - } -} -``` - -## パスワードハッシュ - -```java -@ApplicationScoped -public class PasswordService { - - public String hash(String plainPassword) { - return BcryptUtil.bcryptHash(plainPassword); - } - - public boolean verify(String plainPassword, String hashedPassword) { - return BcryptUtil.matches(plainPassword, hashedPassword); - } -} -``` - -## CORS構成 - -```properties -# application.properties -quarkus.http.cors=true -quarkus.http.cors.origins=https://app.example.com,https://admin.example.com -quarkus.http.cors.methods=GET,POST,PUT,DELETE -quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with -quarkus.http.cors.exposed-headers=Content-Disposition -quarkus.http.cors.access-control-max-age=24H -quarkus.http.cors.access-control-allow-credentials=true -``` - -## シークレット管理 - -```properties -# application.properties — ここにシークレットを置かない - -# 環境変数を使用 -quarkus.datasource.username=${DB_USER} -quarkus.datasource.password=${DB_PASSWORD} -quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET} - -# またはVaultを使用 -quarkus.vault.url=https://vault.example.com -quarkus.vault.authentication.kubernetes.role=my-role -``` - -## レート制限 - -**セキュリティ注意**: `X-Forwarded-For`を直接使用しないでください — クライアントが偽装できます。 -サーブレットリクエストの実際のリモートアドレス、または認証済みIDを使用してください。 - -```java -@ApplicationScoped -public class RateLimitFilter implements ContainerRequestFilter { - private final Map limiters = new ConcurrentHashMap<>(); - - @Inject - HttpServletRequest servletRequest; - - @Override - public void filter(ContainerRequestContext requestContext) { - String clientId = getClientIdentifier(); - RateLimiter limiter = limiters.computeIfAbsent(clientId, - k -> RateLimiter.create(100.0)); // 1秒あたり100リクエスト - - if (!limiter.tryAcquire()) { - requestContext.abortWith( - Response.status(429) - .entity(Map.of("error", "Too many requests")) - .build() - ); - } - } - - private String getClientIdentifier() { - // コンテナ提供のリモートアドレスを使用(X-Forwarded-Forではない)。 - // 信頼できるプロキシの背後にある場合はquarkus.http.proxy.proxy-address-forwarding=trueを設定。 - return servletRequest.getRemoteAddr(); - } -} -``` - -## セキュリティヘッダー - -```java -@Provider -public class SecurityHeadersFilter implements ContainerResponseFilter { - - @Override - public void filter(ContainerRequestContext request, ContainerResponseContext response) { - MultivaluedMap headers = response.getHeaders(); - - // クリックジャッキング防止 - headers.putSingle("X-Frame-Options", "DENY"); - - // XSS保護 - headers.putSingle("X-Content-Type-Options", "nosniff"); - headers.putSingle("X-XSS-Protection", "1; mode=block"); - - // HSTS - headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - - // CSP — script-srcに'unsafe-inline'を使用しないでください。XSS保護が無効になります。 - // 代わりにnonceまたはhashを使用してください。 - headers.putSingle("Content-Security-Policy", - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); - } -} -``` - -## 監査ロギング - -```java -@ApplicationScoped -public class AuditService { - private static final Logger LOG = Logger.getLogger(AuditService.class); - - @Inject - SecurityIdentity securityIdentity; - - public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" - : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", - user, action, resource, Instant.now()); - } -} -``` - -## 依存関係セキュリティスキャン - -```bash -# Maven -mvn org.owasp:dependency-check-maven:check - -# Gradle -./gradlew dependencyCheckAnalyze - -# Quarkusエクステンション確認 -quarkus extension list --installable -``` - -## ベストプラクティス - -- 本番環境では常にHTTPSを使用 -- ステートレス認証にJWTまたはOIDCを有効化 -- 宣言的認可に`@RolesAllowed`を使用 -- Bean Validationですべての入力をバリデーション -- BCryptでパスワードをハッシュ(平文禁止) -- シークレットはVaultまたは環境変数に保存 -- SQLインジェクション防止にパラメータ化クエリを使用 -- すべてのレスポンスにセキュリティヘッダーを追加 -- パブリックエンドポイントにレート制限を実装 -- 機密操作を監査 -- 依存関係を最新に保ちCVEをスキャン -- プログラマティックチェックにSecurityIdentityを使用 -- 適切なCORSポリシーを設定 -- 認証・認可パスをテスト diff --git a/docs/ja-JP/skills/quarkus-tdd/SKILL.md b/docs/ja-JP/skills/quarkus-tdd/SKILL.md deleted file mode 100644 index 97e0bbbd..00000000 --- a/docs/ja-JP/skills/quarkus-tdd/SKILL.md +++ /dev/null @@ -1,390 +0,0 @@ ---- -name: quarkus-tdd -description: Quarkus 3.x LTS向けテスト駆動開発。JUnit 5、Mockito、REST Assured、Camelテスト、JaCoCoを使用。機能追加、バグ修正、イベント駆動サービスのリファクタリングに使用。 -origin: ECC ---- - -# Quarkus TDDワークフロー - -80%以上のカバレッジ(ユニット+統合)を目指すQuarkus 3.xサービスのTDDガイダンス。Apache Camelによるイベント駆動アーキテクチャに最適化。 - -## いつ使用するか - -- 新機能またはRESTエンドポイント -- バグ修正またはリファクタリング -- データアクセスロジック、セキュリティルール、またはリアクティブストリームの追加 -- Apache Camelルートとイベントハンドラーのテスト -- RabbitMQによるイベント駆動サービスのテスト -- 条件付きフローロジックのテスト -- CompletableFuture非同期操作のバリデーション -- LogContext伝播のテスト - -## ワークフロー - -1. まずテストを書く(失敗するはず) -2. テストを通過する最小限のコードを実装 -3. テストがグリーンの状態でリファクタリング -4. JaCoCoでカバレッジを強制(80%以上が目標) - -## @Nestedによるユニットテスト構成 - -包括的で読みやすいテストのための構造化アプローチ: - -```java -@ExtendWith(MockitoExtension.class) -@DisplayName("As2ProcessingService Unit Tests") -class As2ProcessingServiceTest { - - @Mock - private InvoiceFlowValidator invoiceFlowValidator; - - @Mock - private EventService eventService; - - @Mock - private DocumentJobService documentJobService; - - @Mock - private BusinessRulesPublisher businessRulesPublisher; - - @Mock - private FileStorageService fileStorageService; - - @InjectMocks - private As2ProcessingService as2ProcessingService; - - private Path testFilePath; - private LogContext testLogContext; - private InvoiceValidationResult validationResult; - private StoredDocumentInfo documentInfo; - - @BeforeEach - void setUp() { - // ARRANGE - 共通テストデータ - testFilePath = Path.of("/tmp/test-invoice.xml"); - - testLogContext = new LogContext(); - testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); - testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); - testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); - - validationResult = new InvoiceValidationResult(); - validationResult.setValid(true); - validationResult.setSize(1024L); - validationResult.setDocumentHash("abc123"); - - documentInfo = new StoredDocumentInfo(); - documentInfo.setPath("s3://bucket/path/invoice.xml"); - documentInfo.setSize(1024L); - } - - @Nested - @DisplayName("Tests for processFile") - class ProcessFile { - - @Test - @DisplayName("Should successfully process non-CHORUS file with all validations") - void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { - // ARRANGE - testLogContext.put(As2Constants.CHORUS_FLOW, "false"); - CustomLog.setCurrentContext(testLogContext); - - when(invoiceFlowValidator.validateFlowWithConfig( - eq(testFilePath), - eq(ValidationFlowConfig.allValidations()), - eq(EInvoiceSyntaxFormat.UBL), - any(LogContext.class))) - .thenReturn(validationResult); - - when(invoiceFlowValidator.computeFlowProfile(any(), any())) - .thenReturn(FlowProfile.BASIC); - - when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(documentInfo)); - - when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) - .thenReturn(new BusinessRulesPayload()); - - // ACT - assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); - - // ASSERT - verify(invoiceFlowValidator).validateFlowWithConfig( - eq(testFilePath), - eq(ValidationFlowConfig.allValidations()), - eq(EInvoiceSyntaxFormat.UBL), - any(LogContext.class)); - - verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), - eq("PERSISTENCE_BLOB_EVENT_TYPE")); - verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); - } - - @Test - @DisplayName("Should create error event when file upload fails") - void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { - // ARRANGE - testLogContext.put(As2Constants.CHORUS_FLOW, "false"); - CustomLog.setCurrentContext(testLogContext); - - when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) - .thenReturn(validationResult); - - when(invoiceFlowValidator.computeFlowProfile(any(), any())) - .thenReturn(FlowProfile.BASIC); - - documentInfo.setPath(""); // 空パスでエラーをトリガー - when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(documentInfo)); - - // ACT & ASSERT - As2ServerProcessingException exception = assertThrows( - As2ServerProcessingException.class, - () -> as2ProcessingService.processFile(testFilePath) - ); - - assertThat(exception.getMessage()) - .contains("File path is empty after upload"); - - verify(businessRulesPublisher, never()).publishAsync(any()); - } - } -} -``` - -### 主要テストパターン - -1. **@Nestedクラス**: テスト対象メソッドごとにテストをグループ化 -2. **@DisplayName**: テストレポート用の読みやすい説明 -3. **命名規則**: 明確さのために`givenX_whenY_thenZ` -4. **AAAパターン**: 明示的な`// ARRANGE`、`// ACT`、`// ASSERT`コメント -5. **@BeforeEach**: 重複削減のための共通テストデータセットアップ -6. **assertDoesNotThrow**: 例外をキャッチせずに成功シナリオをテスト -7. **assertThrows**: メッセージバリデーション付きの例外シナリオテスト -8. **包括的カバレッジ**: ハッピーパス、null入力、エッジケース、例外をテスト -9. **インタラクション検証**: Mockitoの`verify()`でメソッドが正しく呼ばれることを確認 -10. **Never検証**: エラーシナリオでメソッドが呼ばれないことを`never()`で確認 - -## Camelルートのテスト - -```java -@QuarkusTest -@DisplayName("Business Rules Camel Route Tests") -class BusinessRulesRouteTest { - - @Inject - CamelContext camelContext; - - @Inject - ProducerTemplate producerTemplate; - - @InjectMock - EventService eventService; - - @InjectMock - DocumentValidator documentValidator; - - private BusinessRulesPayload testPayload; - - @BeforeEach - void setUp() { - // ARRANGE - テストデータ - testPayload = new BusinessRulesPayload(); - testPayload.setDocumentId(1L); - testPayload.setFlowProfile(FlowProfile.BASIC); - } - - @Test - @DisplayName("Should successfully publish message to RabbitMQ") - void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { - // ARRANGE - MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); - mockRabbitMQ.expectedMessageCount(1); - - camelContext.getRouteController().stopRoute("business-rules-publisher"); - AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { - advice.replaceFromWith("direct:business-rules-publisher"); - advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); - }); - camelContext.getRouteController().startRoute("business-rules-publisher"); - - // ACT - producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - - // ASSERT - mockRabbitMQ.assertIsSatisfied(5000); - } -} -``` - -## リソースレイヤーテスト(REST Assured) - -```java -@QuarkusTest -@DisplayName("DocumentResource API Tests") -class DocumentResourceTest { - - @InjectMock - DocumentService documentService; - - @Nested - @DisplayName("Tests for POST /api/documents") - class CreateDocument { - - @Test - @DisplayName("Should create document and return 201") - void givenValidRequest_whenCreate_thenReturns201() { - // ARRANGE - Document document = createDocument(1L, "DOC-001"); - when(documentService.create(any())).thenReturn(document); - - // ACT & ASSERT - given() - .contentType(ContentType.JSON) - .body(""" - { - "referenceNumber": "DOC-001", - "description": "Test document", - "validUntil": "2030-01-01T00:00:00Z", - "categories": ["test"] - } - """) - .when().post("/api/documents") - .then() - .statusCode(201) - .body("referenceNumber", equalTo("DOC-001")); - } - - @Test - @DisplayName("Should return 400 for invalid input") - void givenInvalidRequest_whenCreate_thenReturns400() { - given() - .contentType(ContentType.JSON) - .body(""" - { - "referenceNumber": "", - "description": "Test" - } - """) - .when().post("/api/documents") - .then() - .statusCode(400); - } - } -} -``` - -## JaCoCoカバレッジ - -### Maven構成 - -```xml - - org.jacoco - jacoco-maven-plugin - 0.8.13 - - - prepare-agent - prepare-agent - - - report - verify - report - - - check - check - - - - BUNDLE - - - LINE - COVEREDRATIO - 0.80 - - - - - - - - -``` - -カバレッジ付きテスト実行: -```bash -mvn clean test -mvn jacoco:report -mvn jacoco:check - -# レポート: target/site/jacoco/index.html -``` - -## テスト依存関係 - -```xml - - - io.quarkus - quarkus-junit5 - test - - - io.quarkus - quarkus-junit5-mockito - test - - - org.assertj - assertj-core - 3.24.2 - test - - - io.rest-assured - rest-assured - test - - - org.apache.camel.quarkus - camel-quarkus-junit5 - test - - -``` - -## ベストプラクティス - -### テスト構成 -- テスト対象メソッドごとに`@Nested`クラスでグループ化 -- レポートに表示される読みやすい説明に`@DisplayName`を使用 -- テストメソッドに`givenX_whenY_thenZ`命名規則を使用 - -### テスト構造 -- 明示的コメント付きAAAパターン(`// ARRANGE`、`// ACT`、`// ASSERT`) -- 成功シナリオに`assertDoesNotThrow`を使用 -- メッセージバリデーション付き例外シナリオに`assertThrows`を使用 - -### アサーション -- 値チェックにはJUnitアサーションより**AssertJを優先**(`assertThat`) -- 読みやすさのためにAssertJのfluent APIを使用 -- 例外: JUnitの`assertThrows`でキャプチャし、AssertJでメッセージを検証 -- 成功パス: JUnitの`assertDoesNotThrow`を使用 - -### イベント駆動テスト -- `AdviceWith`と`MockEndpoint`でCamelルートをテスト -- メッセージコンテンツ、ヘッダー、ルーティングロジックを検証 -- エラーハンドリングルートを個別にテスト -- ユニットテストで外部システム(RabbitMQ、S3、データベース)をモック - -### Quarkus固有 -- 最新のLTSバージョン(Quarkus 3.x)を維持 -- ネイティブコンパイル互換性を定期的にテスト -- 異なるシナリオにQuarkusテストプロファイルを使用 -- `@MockBean`の代わりに`@InjectMock`を使用(Quarkus固有) - -**覚えておいてください**: テストは高速、分離、決定的に保ちます。実装の詳細ではなく動作をテストしてください。 diff --git a/docs/ja-JP/skills/quarkus-verification/SKILL.md b/docs/ja-JP/skills/quarkus-verification/SKILL.md deleted file mode 100644 index 4e676a8c..00000000 --- a/docs/ja-JP/skills/quarkus-verification/SKILL.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -name: quarkus-verification -description: "Quarkusプロジェクトの検証ループ: ビルド、静的解析、カバレッジ付きテスト、セキュリティスキャン、ネイティブコンパイル、リリースまたはPR前のdiffレビュー。" -origin: ECC ---- - -# Quarkus 検証ループ - -PR前、大きな変更後、デプロイ前に実行。 - -## いつアクティブにするか - -- Quarkusサービスのプルリクエストを開く前 -- 大規模なリファクタリングまたは依存関係アップグレード後 -- ステージングまたは本番のデプロイ前検証 -- 完全なビルド → lint → テスト → セキュリティスキャン → ネイティブコンパイルパイプラインの実行 -- テストカバレッジが閾値を満たしていることの検証(80%以上) -- ネイティブイメージ互換性のテスト - -## フェーズ1: ビルド - -```bash -# Maven -mvn clean verify -DskipTests - -# Gradle -./gradlew clean assemble -x test -``` - -ビルドが失敗した場合、停止してコンパイルエラーを修正。 - -## フェーズ2: 静的解析 - -### Checkstyle、PMD、SpotBugs(Maven) - -```bash -mvn checkstyle:check pmd:check spotbugs:check -``` - -### SonarQube(構成されている場合) - -```bash -mvn sonar:sonar \ - -Dsonar.projectKey=my-quarkus-project \ - -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.login=${SONAR_TOKEN} -``` - -### 対処すべき一般的な問題 - -- 未使用のimportまたは変数 -- 複雑なメソッド(高い循環的複雑度) -- 潜在的なnullポインター参照 -- SpotBugsが検出したセキュリティ問題 - -## フェーズ3: テスト + カバレッジ - -```bash -# すべてのテストを実行 -mvn clean test - -# カバレッジレポートを生成 -mvn jacoco:report - -# カバレッジ閾値を強制(80%) -mvn jacoco:check - -# またはGradleで -./gradlew test jacocoTestReport jacocoTestCoverageVerification -``` - -### テストカテゴリ - -#### ユニットテスト -モック化された依存関係でサービスロジックをテスト: - -```java -@ExtendWith(MockitoExtension.class) -class UserServiceTest { - @Mock UserRepository userRepository; - @InjectMocks UserService userService; - - @Test - void createUser_validInput_returnsUser() { - var dto = new CreateUserDto("Alice", "alice@example.com"); - - // Panacheのpersist()はvoidを返す — doNothing + verifyを使用 - doNothing().when(userRepository).persist(any(User.class)); - - User result = userService.create(dto); - - assertThat(result.name).isEqualTo("Alice"); - verify(userRepository).persist(any(User.class)); - } -} -``` - -#### 統合テスト -実データベース(Testcontainers)でテスト: - -```java -@QuarkusTest -@QuarkusTestResource(PostgresTestResource.class) -class UserRepositoryIntegrationTest { - - @Inject - UserRepository userRepository; - - @Test - @Transactional - void findByEmail_existingUser_returnsUser() { - User user = new User(); - user.name = "Alice"; - user.email = "alice@example.com"; - userRepository.persist(user); - - Optional found = userRepository.findByEmail("alice@example.com"); - - assertThat(found).isPresent(); - assertThat(found.get().name).isEqualTo("Alice"); - } -} -``` - -#### APIテスト -REST AssuredでRESTエンドポイントをテスト: - -```java -@QuarkusTest -class UserResourceTest { - - @Test - void createUser_validInput_returns201() { - given() - .contentType(ContentType.JSON) - .body(""" - {"name": "Alice", "email": "alice@example.com"} - """) - .when().post("/api/users") - .then() - .statusCode(201) - .body("name", equalTo("Alice")); - } - - @Test - void createUser_invalidEmail_returns400() { - given() - .contentType(ContentType.JSON) - .body(""" - {"name": "Alice", "email": "invalid"} - """) - .when().post("/api/users") - .then() - .statusCode(400); - } -} -``` - -### カバレッジレポート - -詳細カバレッジは`target/site/jacoco/index.html`を確認: -- 全体行カバレッジ(目標: 80%以上) -- ブランチカバレッジ(目標: 70%以上) -- カバーされていない重要パスを特定 - -## フェーズ4: セキュリティスキャン - -### 依存関係脆弱性(Maven) - -```bash -mvn org.owasp:dependency-check-maven:check -``` - -CVEについて`target/dependency-check-report.html`を確認。 - -### Quarkusセキュリティ監査 - -```bash -# 脆弱なエクステンションを確認 -mvn quarkus:audit - -# すべてのエクステンションをリスト -mvn quarkus:list-extensions -``` - -### 一般的なセキュリティチェック - -- [ ] すべてのシークレットが環境変数に(コード内ではなく) -- [ ] すべてのエンドポイントで入力バリデーション -- [ ] 認証/認可が構成済み -- [ ] CORSが適切に構成済み -- [ ] セキュリティヘッダーが設定済み -- [ ] パスワードがBCryptでハッシュ済み -- [ ] SQLインジェクション保護(パラメータ化クエリ) -- [ ] パブリックエンドポイントでレート制限 - -## フェーズ5: ネイティブコンパイル - -GraalVMネイティブイメージ互換性をテスト: - -```bash -# ネイティブ実行可能ファイルをビルド -mvn package -Dnative - -# またはコンテナで -mvn package -Dnative -Dquarkus.native.container-build=true - -# ネイティブ実行可能ファイルをテスト -./target/*-runner - -# 基本的なスモークテストを実行 -curl http://localhost:8080/q/health/live -curl http://localhost:8080/q/health/ready -``` - -### ネイティブイメージトラブルシューティング - -一般的な問題: -- **Reflection**: 動的クラス用のreflection構成を追加 -- **Resources**: `quarkus.native.resources.includes`でリソースを含める -- **JNI**: ネイティブライブラリ使用時にJNIクラスを登録 - -例のreflection構成: -```java -@RegisterForReflection(targets = {MyDynamicClass.class}) -public class ReflectionConfiguration {} -``` - -## フェーズ6: ヘルスチェック - -```bash -# Liveness -curl http://localhost:8080/q/health/live - -# Readiness -curl http://localhost:8080/q/health/ready - -# すべてのヘルスチェック -curl http://localhost:8080/q/health - -# メトリクス(有効な場合) -curl http://localhost:8080/q/metrics -``` - -## 検証チェックリスト - -### コード品質 -- [ ] ビルドが警告なしで通過 -- [ ] 静的解析クリーン(高/中の問題なし) -- [ ] コードがチーム規約に従う -- [ ] PRにコメントアウトされたコードやTODOがない - -### テスト -- [ ] すべてのテストが通過 -- [ ] コードカバレッジ ≥ 80% -- [ ] 実データベースとの統合テスト -- [ ] セキュリティテストが通過 -- [ ] パフォーマンスが許容範囲内 - -### セキュリティ -- [ ] 依存関係脆弱性なし -- [ ] 認証/認可がテスト済み -- [ ] 入力バリデーション完了 -- [ ] ソースコードにシークレットなし -- [ ] セキュリティヘッダーが構成済み - -### デプロイメント -- [ ] ネイティブコンパイル成功 -- [ ] コンテナイメージがビルド可能 -- [ ] ヘルスチェックが正しく応答 -- [ ] ターゲット環境で構成が有効 - -## 自動検証スクリプト - -```bash -#!/bin/bash -set -e - -echo "=== フェーズ1: ビルド ===" -mvn clean verify -DskipTests - -echo "=== フェーズ2: 静的解析 ===" -mvn checkstyle:check pmd:check spotbugs:check - -echo "=== フェーズ3: テスト + カバレッジ ===" -mvn test jacoco:report jacoco:check - -echo "=== フェーズ4: セキュリティスキャン ===" -mvn org.owasp:dependency-check-maven:check - -echo "=== フェーズ5: ネイティブコンパイル ===" -mvn package -Dnative -Dquarkus.native.container-build=true - -echo "=== 全フェーズ完了 ===" -echo "レポートを確認:" -echo " - カバレッジ: target/site/jacoco/index.html" -echo " - セキュリティ: target/dependency-check-report.html" -echo " - ネイティブ: target/*-runner" -``` - -## ベストプラクティス - -- すべてのPR前に検証ループを実行 -- CI/CDパイプラインで自動化 -- 問題を即座に修正し、技術的負債を蓄積しない -- カバレッジを80%以上に維持 -- 依存関係を定期的に更新 -- ネイティブコンパイルを定期的にテスト -- パフォーマンストレンドを監視 -- 破壊的変更を文書化 -- セキュリティスキャン結果をレビュー -- 各環境の構成を検証 diff --git a/docs/zh-CN/skills/quarkus-patterns/SKILL.md b/docs/zh-CN/skills/quarkus-patterns/SKILL.md deleted file mode 100644 index 50af74a0..00000000 --- a/docs/zh-CN/skills/quarkus-patterns/SKILL.md +++ /dev/null @@ -1,740 +0,0 @@ ---- -name: quarkus-patterns -description: Quarkus 3.x LTS架构模式,Camel消息传递、RESTful API设计、CDI服务、Panache数据访问和异步处理。用于具有事件驱动架构的Java Quarkus后端工作。 -origin: ECC ---- - -# Quarkus 开发模式 - -使用Apache Camel的云原生事件驱动服务的Quarkus 3.x架构和API模式。 - -## 何时激活 - -- 使用JAX-RS或RESTEasy Reactive构建REST API -- 构建资源 → 服务 → 仓库层结构 -- 使用Apache Camel和RabbitMQ实现事件驱动模式 -- 配置Hibernate Panache、缓存或响应式流 -- 添加验证、异常映射或分页 -- 为开发/预发布/生产环境设置配置文件(YAML配置) -- 使用LogContext和Logback/Logstash编码器进行自定义日志记录 -- 使用CompletableFuture进行异步操作 -- 实现条件流处理 -- 使用GraalVM原生编译 - -## 多依赖服务层(Lombok) - -```java -@Slf4j -@ApplicationScoped -@RequiredArgsConstructor -public class As2ProcessingService { - - private final InvoiceFlowValidator invoiceFlowValidator; - private final EventService eventService; - private final DocumentJobService documentJobService; - private final BusinessRulesPublisher businessRulesPublisher; - private final FileStorageService fileStorageService; - - public void processFile(Path filePath) throws Exception { - LogContext logContext = CustomLog.getCurrentContext(); - try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { - - String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID); - - // 条件流逻辑 - boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW)); - log.info("Is CHORUS_FLOW message: {}", isChorusFlow); - - ValidationFlowConfig validationFlowConfig = isChorusFlow - ? ValidationFlowConfig.xsdOnly() - : ValidationFlowConfig.allValidations(); - - InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator - .validateFlowWithConfig(filePath, validationFlowConfig, - EInvoiceSyntaxFormat.UBL, logContext); - - FlowProfile flowProfile = isChorusFlow ? - FlowProfile.EXTENDED_CTC_FR : - this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult, - invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile()); - - log.info("Invoice validation completed. Message is valid"); - - // CompletableFuture异步操作 - try(InputStream inputStream = Files.newInputStream(filePath)) { - CompletableFuture documentInfoCompletableFuture = - fileStorageService.uploadOriginalFile(inputStream, - invoiceValidationResult.getSize(), logContext, - invoiceValidationResult.getInvoiceFormat()); - - StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join(); - log.info("File uploaded successfully: {}", documentInfo.getPath()); - - if (StringUtils.isBlank(documentInfo.getPath())) { - String errorMsg = "File path is empty after upload"; - log.error(errorMsg); - this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg); - throw new As2ServerProcessingException(errorMsg); - } - - this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE"); - - BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities( - documentInfo, originalFileName, structureIdPartner, - flowProfile, invoiceValidationResult.getDocumentHash()); - - // 异步Camel发布 - businessRulesPublisher.publishAsync(payload); - this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT"); - } - } - } -} -``` - -**关键模式:** -- 通过Lombok的`@RequiredArgsConstructor`进行构造函数注入 -- 通过`@Slf4j`进行Logback日志记录 -- 使用try-with-resources的作用域LogContext -- 基于运行时参数的条件流逻辑 -- 使用`.join()`的CompletableFuture异步操作 -- 成功/错误场景的事件跟踪 -- 异步Camel消息发布 - -## 自定义日志上下文模式(Logback) - -```java -@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("Starting document processing"); - - // 此作用域内的所有日志都继承上下文 - processInternal(doc); - - log.info("Document processing completed"); - } catch (Exception e) { - log.error("Document processing failed", e); - throw e; - } - } -} -``` - -**Logback配置(logback.xml):** - -```xml - - - - true - true - - - - - - - - -``` - -## 事件服务模式 - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class EventService { - private final EventRepository eventRepository; - private final ObjectMapper objectMapper; - - public void createSuccessEvent(Object payload, String eventType) { - Objects.requireNonNull(payload, "Payload cannot be 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("Success event created: {}", eventType); - } - - public void createErrorEvent(Object payload, String eventType, String errorMessage) { - Objects.requireNonNull(payload, "Payload cannot be null"); - if (errorMessage == null || errorMessage.isBlank()) { - throw new IllegalArgumentException("Error message cannot be blank"); - } - 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("Error event created: {} - {}", eventType, errorMessage); - } - - private String serializePayload(Object payload) { - try { - return objectMapper.writeValueAsString(payload); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Failed to serialize event payload", e); - } - } -} -``` - -## Camel消息发布(RabbitMQ) - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class BusinessRulesPublisher { - private final ProducerTemplate producerTemplate; - - @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") - String businessRulesQueue; - - public void publishAsync(BusinessRulesPayload payload) { - producerTemplate.asyncSendBody( - "direct:business-rules-publisher", - payload - ); - log.info("Message published to business rules queue: {}", payload.getDocumentId()); - } - - public void publishSync(BusinessRulesPayload payload) { - producerTemplate.sendBody( - "direct:business-rules-publisher", - payload - ); - } -} -``` - -**Camel路由配置:** - -```java -@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("Publishing message to RabbitMQ: ${body}") - .marshal().json(JsonLibrary.Jackson) - .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", - businessRulesQueue, rabbitHost, rabbitPort); - } -} -``` - -## Camel Direct路由(内存中) - -```java -@ApplicationScoped -public class DocumentProcessingRoute extends RouteBuilder { - - @Override - public void configure() { - // 错误处理 - onException(ValidationException.class) - .handled(true) - .to("direct:validation-error-handler") - .log("Validation error: ${exception.message}"); - - // 主处理路由 - from("direct:process-document") - .routeId("document-processing") - .log("Processing document: ${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("Validation error handled"); - } -} -``` - -## Camel文件处理 - -```java -@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("Processing file: ${header.CamelFileName}") - .to("direct:process-file"); - - from("direct:process-file") - .bean(As2ProcessingService.class, "processFile") - .log("File processing completed"); - } -} -``` - -## REST API结构 - -```java -@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 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(); - } -} -``` - -## 仓库模式(Panache Repository) - -```java -@ApplicationScoped -public class DocumentRepository implements PanacheRepository { - - public List findByStatus(DocumentStatus status, int page, int size) { - return find("status = ?1 order by createdAt desc", status) - .page(page, size) - .list(); - } - - public Optional 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()); - } -} -``` - -## 带事务的服务层 - -```java -@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 findById(Long id) { - return repo.findByIdOptional(id); - } -} -``` - -## DTO和验证 - -```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()); - } -} -``` - -## 异常映射 - -```java -@Provider -public class ValidationExceptionMapper implements ExceptionMapper { - @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 { - - @Override - public Response toResponse(Exception exception) { - log.error("Unhandled exception", exception); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "internal_error", "message", "An unexpected error occurred")) - .build(); - } -} -``` - -## CompletableFuture异步操作 - -```java -@Slf4j -@ApplicationScoped -@RequiredArgsConstructor -public class FileStorageService { - private final S3Client s3Client; - private final ExecutorService executorService; - - @ConfigProperty(name = "storage.bucket-name") String bucketName; - - public CompletableFuture 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)); - - log.info("File uploaded to S3: {}", path); - - return new StoredDocumentInfo(path, size, Instant.now()); - } catch (Exception e) { - log.error("Failed to upload file to S3", e); - throw new StorageException("Upload failed", e); - } - }, executorService); - } -} -``` - -## 缓存 - -```java -@ApplicationScoped -@RequiredArgsConstructor -public class DocumentCacheService { - private final DocumentRepository repo; - - @CacheResult(cacheName = "document-cache") - public Optional getById(@CacheKey Long id) { - return repo.findByIdOptional(id); - } - - @CacheInvalidate(cacheName = "document-cache") - public void evict(@CacheKey Long id) {} - - @CacheInvalidateAll(cacheName = "document-cache") - public void evictAll() {} -} -``` - -## YAML配置 - -```yaml -# application.yml -"%dev": - quarkus: - datasource: - jdbc: - url: jdbc:postgresql://localhost:5432/dev_db - username: dev_user - password: dev_pass - hibernate-orm: - database: - generation: drop-and-create - - rabbitmq: - host: localhost - port: 5672 - username: guest - password: guest - -"%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} - hibernate-orm: - database: - generation: validate - - rabbitmq: - host: ${RABBITMQ_HOST} - port: ${RABBITMQ_PORT} - username: ${RABBITMQ_USER} - password: ${RABBITMQ_PASSWORD} - -# Camel配置 -camel: - rabbitmq: - queue: - business-rules: business-rules-queue - invoice-processing: invoice-processing-queue -``` - -## 健康检查 - -```java -@Readiness -@ApplicationScoped -@RequiredArgsConstructor -public 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"); - } - } -} - -@Liveness -@ApplicationScoped -public class CamelHealthCheck implements HealthCheck { - @Inject - CamelContext camelContext; - - @Override - public HealthCheckResponse call() { - boolean isStarted = camelContext.getStatus().isStarted(); - return HealthCheckResponse.named("Camel Context") - .status(isStarted) - .build(); - } -} -``` - -## 依赖(Maven) - -```xml - - 3.27.0 - 1.18.42 - 3.24.2 - 0.8.13 - 17 - - - - - - io.quarkus.platform - quarkus-bom - ${quarkus.platform.version} - pom - import - - - io.quarkus.platform - quarkus-camel-bom - ${quarkus.platform.version} - pom - import - - - - - - - - io.quarkus - quarkus-arc - - - io.quarkus - quarkus-config-yaml - - - - - org.apache.camel.quarkus - camel-quarkus-spring-rabbitmq - - - org.apache.camel.quarkus - camel-quarkus-direct - - - org.apache.camel.quarkus - camel-quarkus-bean - - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - io.quarkiverse.logging.logback - quarkus-logging-logback - - - net.logstash.logback - logstash-logback-encoder - - -``` - -## 最佳实践 - -### 架构 -- 使用Lombok的`@RequiredArgsConstructor`进行构造函数注入 -- 保持服务层精简,将复杂逻辑委托给专门的类 -- 使用Camel路由进行消息路由和集成模式 -- 数据访问优先使用Panache Repository模式 - -### 事件驱动 -- 始终使用EventService跟踪操作(成功/错误事件) -- 使用Camel的`direct:`端点进行内存路由 -- 使用`spring-rabbitmq`组件进行RabbitMQ集成 -- 使用`ProducerTemplate.asyncSendBody()`实现异步发布 - -### 日志 -- 使用Logstash编码器的Logback进行结构化日志 -- 使用`SafeAutoCloseable`在服务调用间传播LogContext -- 向LogContext添加上下文信息以进行请求追踪 -- 使用`@Slf4j`代替手动日志实例化 - -### 异步操作 -- 使用CompletableFuture进行非阻塞I/O操作 -- 需要等待完成时调用`.join()` -- 正确处理CompletableFuture的异常 -- 为追踪目的向异步操作传递LogContext - -### 配置 -- 使用YAML配置(`quarkus-config-yaml`) -- dev/test/prod环境的配置文件感知配置 -- 将敏感配置外部化到环境变量 -- 使用`@ConfigProperty`进行类型安全的配置注入 - -### 验证 -- 在资源层使用`@Valid`进行验证 -- 在DTO上使用Bean Validation注解 -- 使用`@Provider`将异常映射到适当的HTTP响应 - -### 事务 -- 在修改数据的服务方法上使用`@Transactional` -- 保持事务短小且聚焦 -- 避免在事务内调用异步操作 - -### 测试 -- 使用`camel-quarkus-junit5`进行路由测试 -- 使用AssertJ进行断言 -- 模拟所有外部依赖 -- 彻底测试条件流逻辑 - -### Quarkus特定 -- 保持最新的LTS版本(3.x) -- 使用Quarkus开发模式进行热重载 -- 添加健康检查以确保生产就绪 -- 定期测试原生编译兼容性 diff --git a/docs/zh-CN/skills/quarkus-security/SKILL.md b/docs/zh-CN/skills/quarkus-security/SKILL.md deleted file mode 100644 index 5b2ff96a..00000000 --- a/docs/zh-CN/skills/quarkus-security/SKILL.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -name: quarkus-security -description: Quarkus安全最佳实践:认证、授权、JWT/OIDC、RBAC、输入验证、CSRF、密钥管理和依赖安全。 -origin: ECC ---- - -# Quarkus 安全审查 - -使用认证、授权和输入验证保护Quarkus应用程序的最佳实践。 - -## 何时激活 - -- 添加认证(JWT、OIDC、Basic Auth) -- 使用@RolesAllowed或SecurityIdentity实现授权 -- 验证用户输入(Bean Validation、自定义验证器) -- 配置CORS或安全头 -- 管理密钥(Vault、环境变量、配置源) -- 添加速率限制或暴力破解保护 -- 扫描依赖CVE -- 使用MicroProfile JWT或SmallRye JWT - -## 认证 - -### JWT认证 - -```java -@Path("/api/protected") -@Authenticated -public class ProtectedResource { - - @Inject - JsonWebToken jwt; - - @Inject - SecurityIdentity securityIdentity; - - @GET - public Response getData() { - String username = jwt.getName(); - Set roles = jwt.getGroups(); - return Response.ok(Map.of( - "username", username, - "roles", roles, - "principal", securityIdentity.getPrincipal().getName() - )).build(); - } -} -``` - -配置(application.properties): -```properties -mp.jwt.verify.publickey.location=publicKey.pem -mp.jwt.verify.issuer=https://auth.example.com - -# OIDC -quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm -quarkus.oidc.client-id=backend-service -quarkus.oidc.credentials.secret=${OIDC_SECRET} -``` - -### 自定义认证过滤器 - -```java -@Provider -@Priority(Priorities.AUTHENTICATION) -public class CustomAuthFilter implements ContainerRequestFilter { - - @Inject - SecurityIdentity identity; - - @Override - public void filter(ContainerRequestContext requestContext) { - String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - - // 头部缺失或格式错误时立即拒绝 - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - return; - } - - String token = authHeader.substring(7); - if (!validateToken(token)) { - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - } - } - - private boolean validateToken(String token) { - // 令牌验证逻辑 - return true; - } -} -``` - -## 授权 - -### 基于角色的访问控制 - -```java -@Path("/api/admin") -@RolesAllowed("ADMIN") -public class AdminResource { - - @GET - @Path("/users") - public List listUsers() { - return userService.findAll(); - } - - @DELETE - @Path("/users/{id}") - @RolesAllowed({"ADMIN", "SUPER_ADMIN"}) - public Response deleteUser(@PathParam("id") Long id) { - userService.delete(id); - return Response.noContent().build(); - } -} - -@Path("/api/users") -public class UserResource { - - @Inject - SecurityIdentity securityIdentity; - - @GET - @Path("/{id}") - @RolesAllowed("USER") - public Response getUser(@PathParam("id") Long id) { - // 所有权检查 - if (!securityIdentity.hasRole("ADMIN") && - !isOwner(id, securityIdentity.getPrincipal().getName())) { - return Response.status(Response.Status.FORBIDDEN).build(); - } - return Response.ok(userService.findById(id)).build(); - } -} -``` - -### 编程式安全 - -```java -@ApplicationScoped -public class SecurityService { - - @Inject - SecurityIdentity securityIdentity; - - public boolean canAccessResource(Long resourceId) { - if (securityIdentity.isAnonymous()) { - return false; - } - - if (securityIdentity.hasRole("ADMIN")) { - return true; - } - - String userId = securityIdentity.getPrincipal().getName(); - return resourceRepository.isOwner(resourceId, userId); - } -} -``` - -## 输入验证 - -### Bean Validation - -```java -// BAD: 无验证 -@POST -public Response createUser(UserDto dto) { - return Response.ok(userService.create(dto)).build(); -} - -// GOOD: 验证DTO -public record CreateUserDto( - @NotBlank @Size(max = 100) String name, - @NotBlank @Email String email, - @NotNull @Min(18) @Max(150) Integer age, - @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone -) {} - -@POST -@Path("/users") -public Response createUser(@Valid CreateUserDto dto) { - User user = userService.create(dto); - return Response.status(Response.Status.CREATED).entity(user).build(); -} -``` - -## SQL注入防护 - -### Panache Active Record(默认安全) - -```java -// GOOD: Panache参数化查询 -List users = User.list("email = ?1 and active = ?2", email, true); - -Optional user = User.find("username", username).firstResultOptional(); - -// GOOD: 命名参数 -List users = User.list("email = :email and age > :minAge", - Parameters.with("email", email).and("minAge", 18)); -``` - -### 原生查询(使用参数) - -```java -// BAD: 字符串拼接 -@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true) - -// GOOD: 参数化原生查询 -@Entity -public class User extends PanacheEntity { - public static List findByEmailNative(String email) { - return getEntityManager() - .createNativeQuery("SELECT * FROM users WHERE email = :email", User.class) - .setParameter("email", email) - .getResultList(); - } -} -``` - -## 密码哈希 - -```java -@ApplicationScoped -public class PasswordService { - - public String hash(String plainPassword) { - return BcryptUtil.bcryptHash(plainPassword); - } - - public boolean verify(String plainPassword, String hashedPassword) { - return BcryptUtil.matches(plainPassword, hashedPassword); - } -} -``` - -## CORS配置 - -```properties -# application.properties -quarkus.http.cors=true -quarkus.http.cors.origins=https://app.example.com,https://admin.example.com -quarkus.http.cors.methods=GET,POST,PUT,DELETE -quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with -quarkus.http.cors.exposed-headers=Content-Disposition -quarkus.http.cors.access-control-max-age=24H -quarkus.http.cors.access-control-allow-credentials=true -``` - -## 密钥管理 - -```properties -# application.properties — 此处不放密钥 - -# 使用环境变量 -quarkus.datasource.username=${DB_USER} -quarkus.datasource.password=${DB_PASSWORD} -quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET} - -# 或使用Vault -quarkus.vault.url=https://vault.example.com -quarkus.vault.authentication.kubernetes.role=my-role -``` - -## 速率限制 - -```java -@ApplicationScoped -public class RateLimitFilter implements ContainerRequestFilter { - private final Map limiters = new ConcurrentHashMap<>(); - - @Override - public void filter(ContainerRequestContext requestContext) { - String clientId = getClientIdentifier(requestContext); - RateLimiter limiter = limiters.computeIfAbsent(clientId, - k -> RateLimiter.create(100.0)); // 每秒100个请求 - - if (!limiter.tryAcquire()) { - requestContext.abortWith( - Response.status(429) - .entity(Map.of("error", "Too many requests")) - .build() - ); - } - } -} -``` - -## 安全头 - -```java -@Provider -public class SecurityHeadersFilter implements ContainerResponseFilter { - - @Override - public void filter(ContainerRequestContext request, ContainerResponseContext response) { - MultivaluedMap headers = response.getHeaders(); - - // 防止点击劫持 - headers.putSingle("X-Frame-Options", "DENY"); - - // XSS保护 - headers.putSingle("X-Content-Type-Options", "nosniff"); - headers.putSingle("X-XSS-Protection", "1; mode=block"); - - // HSTS - headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - - // CSP — script-src不要使用'unsafe-inline',会使XSS保护失效; - // 请改用nonce或hash - headers.putSingle("Content-Security-Policy", - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); - } -} -``` - -## 审计日志 - -```java -@ApplicationScoped -public class AuditService { - private static final Logger LOG = Logger.getLogger(AuditService.class); - - @Inject - SecurityIdentity securityIdentity; - - public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" - : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", - user, action, resource, Instant.now()); - } -} -``` - -## 依赖安全扫描 - -```bash -# Maven -mvn org.owasp:dependency-check-maven:check - -# Gradle -./gradlew dependencyCheckAnalyze - -# 检查Quarkus扩展 -quarkus extension list --installable -``` - -## 最佳实践 - -- 生产环境始终使用HTTPS -- 启用JWT或OIDC进行无状态认证 -- 使用`@RolesAllowed`进行声明式授权 -- 使用Bean Validation验证所有输入 -- 使用BCrypt哈希密码(禁止明文) -- 将密钥存储在Vault或环境变量中 -- 使用参数化查询防止SQL注入 -- 为所有响应添加安全头 -- 为公共端点实现速率限制 -- 审计敏感操作 -- 保持依赖更新并扫描CVE -- 使用SecurityIdentity进行编程式检查 -- 设置适当的CORS策略 -- 测试认证和授权路径 diff --git a/docs/zh-CN/skills/quarkus-tdd/SKILL.md b/docs/zh-CN/skills/quarkus-tdd/SKILL.md deleted file mode 100644 index 3e12f505..00000000 --- a/docs/zh-CN/skills/quarkus-tdd/SKILL.md +++ /dev/null @@ -1,389 +0,0 @@ ---- -name: quarkus-tdd -description: 使用JUnit 5、Mockito、REST Assured、Camel测试和JaCoCo的Quarkus 3.x LTS测试驱动开发。用于添加功能、修复错误或重构事件驱动服务。 -origin: ECC ---- - -# Quarkus TDD工作流 - -面向80%以上覆盖率(单元+集成)的Quarkus 3.x服务TDD指南。针对Apache Camel的事件驱动架构优化。 - -## 何时使用 - -- 新功能或REST端点 -- Bug修复或重构 -- 添加数据访问逻辑、安全规则或响应式流 -- 测试Apache Camel路由和事件处理器 -- 测试RabbitMQ事件驱动服务 -- 测试条件流逻辑 -- 验证CompletableFuture异步操作 -- 测试LogContext传播 - -## 工作流 - -1. 先写测试(应该失败) -2. 实现通过测试的最少代码 -3. 测试通过后重构 -4. 使用JaCoCo强制覆盖率(80%以上目标) - -## 使用@Nested组织的单元测试 - -全面、可读测试的结构化方法: - -```java -@ExtendWith(MockitoExtension.class) -@DisplayName("As2ProcessingService Unit Tests") -class As2ProcessingServiceTest { - - @Mock - private InvoiceFlowValidator invoiceFlowValidator; - - @Mock - private EventService eventService; - - @Mock - private DocumentJobService documentJobService; - - @Mock - private BusinessRulesPublisher businessRulesPublisher; - - @Mock - private FileStorageService fileStorageService; - - @InjectMocks - private As2ProcessingService as2ProcessingService; - - private Path testFilePath; - private LogContext testLogContext; - private InvoiceValidationResult validationResult; - private StoredDocumentInfo documentInfo; - - @BeforeEach - void setUp() { - // ARRANGE - 公共测试数据 - testFilePath = Path.of("/tmp/test-invoice.xml"); - - testLogContext = new LogContext(); - testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); - testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); - testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); - - validationResult = new InvoiceValidationResult(); - validationResult.setValid(true); - validationResult.setSize(1024L); - validationResult.setDocumentHash("abc123"); - - documentInfo = new StoredDocumentInfo(); - documentInfo.setPath("s3://bucket/path/invoice.xml"); - documentInfo.setSize(1024L); - } - - @Nested - @DisplayName("Tests for processFile") - class ProcessFile { - - @Test - @DisplayName("Should successfully process non-CHORUS file with all validations") - void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { - // ARRANGE - testLogContext.put(As2Constants.CHORUS_FLOW, "false"); - CustomLog.setCurrentContext(testLogContext); - - when(invoiceFlowValidator.validateFlowWithConfig( - eq(testFilePath), - eq(ValidationFlowConfig.allValidations()), - eq(EInvoiceSyntaxFormat.UBL), - any(LogContext.class))) - .thenReturn(validationResult); - - when(invoiceFlowValidator.computeFlowProfile(any(), any())) - .thenReturn(FlowProfile.BASIC); - - when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(documentInfo)); - - when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) - .thenReturn(new BusinessRulesPayload()); - - // ACT - assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); - - // ASSERT - verify(invoiceFlowValidator).validateFlowWithConfig( - eq(testFilePath), - eq(ValidationFlowConfig.allValidations()), - eq(EInvoiceSyntaxFormat.UBL), - any(LogContext.class)); - - verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), - eq("PERSISTENCE_BLOB_EVENT_TYPE")); - verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); - } - - @Test - @DisplayName("Should create error event when file upload fails") - void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { - // ARRANGE - testLogContext.put(As2Constants.CHORUS_FLOW, "false"); - CustomLog.setCurrentContext(testLogContext); - - when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) - .thenReturn(validationResult); - - when(invoiceFlowValidator.computeFlowProfile(any(), any())) - .thenReturn(FlowProfile.BASIC); - - documentInfo.setPath(""); // 空路径触发错误 - when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(documentInfo)); - - // ACT & ASSERT - As2ServerProcessingException exception = assertThrows( - As2ServerProcessingException.class, - () -> as2ProcessingService.processFile(testFilePath) - ); - - assertThat(exception.getMessage()) - .contains("File path is empty after upload"); - - verify(businessRulesPublisher, never()).publishAsync(any()); - } - } -} -``` - -### 关键测试模式 - -1. **@Nested类**: 按被测方法分组测试 -2. **@DisplayName**: 为测试报告提供可读描述 -3. **命名约定**: 使用`givenX_whenY_thenZ`确保清晰 -4. **AAA模式**: 明确的`// ARRANGE`、`// ACT`、`// ASSERT`注释 -5. **@BeforeEach**: 通用测试数据设置以减少重复 -6. **assertDoesNotThrow**: 不捕获异常的成功场景测试 -7. **assertThrows**: 带消息验证的异常场景测试 -8. **全面覆盖**: 测试正常路径、null输入、边界情况、异常 -9. **验证交互**: 使用Mockito的`verify()`确保方法被正确调用 -10. **Never验证**: 使用`never()`确保错误场景中方法未被调用 - -## 测试Camel路由 - -```java -@QuarkusTest -@DisplayName("Business Rules Camel Route Tests") -class BusinessRulesRouteTest { - - @Inject - CamelContext camelContext; - - @Inject - ProducerTemplate producerTemplate; - - @InjectMock - EventService eventService; - - @InjectMock - DocumentValidator documentValidator; - - private BusinessRulesPayload testPayload; - - @BeforeEach - void setUp() { - // ARRANGE - 测试数据 - testPayload = new BusinessRulesPayload(); - testPayload.setDocumentId(1L); - testPayload.setFlowProfile(FlowProfile.BASIC); - } - - @Test - @DisplayName("Should successfully publish message to RabbitMQ") - void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { - // ARRANGE - MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); - mockRabbitMQ.expectedMessageCount(1); - - camelContext.getRouteController().stopRoute("business-rules-publisher"); - AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { - advice.replaceFromWith("direct:business-rules-publisher"); - advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); - }); - camelContext.getRouteController().startRoute("business-rules-publisher"); - - // ACT - producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - - // ASSERT - mockRabbitMQ.assertIsSatisfied(5000); - } -} -``` - -## 资源层测试(REST Assured) - -```java -@QuarkusTest -@DisplayName("DocumentResource API Tests") -class DocumentResourceTest { - - @InjectMock - DocumentService documentService; - - @Nested - @DisplayName("Tests for POST /api/documents") - class CreateDocument { - - @Test - @DisplayName("Should create document and return 201") - void givenValidRequest_whenCreate_thenReturns201() { - // ARRANGE - Document document = createDocument(1L, "DOC-001"); - when(documentService.create(any())).thenReturn(document); - - // ACT & ASSERT - given() - .contentType(ContentType.JSON) - .body(""" - { - "referenceNumber": "DOC-001", - "description": "Test document", - "validUntil": "2030-01-01T00:00:00Z", - "categories": ["test"] - } - """) - .when().post("/api/documents") - .then() - .statusCode(201) - .body("referenceNumber", equalTo("DOC-001")); - } - - @Test - @DisplayName("Should return 400 for invalid input") - void givenInvalidRequest_whenCreate_thenReturns400() { - given() - .contentType(ContentType.JSON) - .body(""" - { - "referenceNumber": "", - "description": "Test" - } - """) - .when().post("/api/documents") - .then() - .statusCode(400); - } - } -} -``` - -## JaCoCo覆盖率 - -### Maven配置 - -```xml - - org.jacoco - jacoco-maven-plugin - 0.8.13 - - - prepare-agent - prepare-agent - - - report - verify - report - - - check - check - - - - BUNDLE - - - LINE - COVEREDRATIO - 0.80 - - - - - - - - -``` - -运行带覆盖率的测试: -```bash -mvn clean test -mvn jacoco:report -mvn jacoco:check - -# 报告位于: target/site/jacoco/index.html -``` - -## 测试依赖 - -```xml - - - io.quarkus - quarkus-junit5 - test - - - io.quarkus - quarkus-junit5-mockito - test - - - org.assertj - assertj-core - 3.24.2 - test - - - io.rest-assured - rest-assured - test - - - org.apache.camel.quarkus - camel-quarkus-junit5 - test - - -``` - -## 最佳实践 - -### 测试组织 -- 使用`@Nested`类按被测方法分组 -- 使用`@DisplayName`提供可读的测试描述 -- 遵循`givenX_whenY_thenZ`命名约定 - -### 测试结构 -- 遵循带明确注释的AAA模式(`// ARRANGE`、`// ACT`、`// ASSERT`) -- 成功场景使用`assertDoesNotThrow` -- 异常场景使用`assertThrows`并验证消息 - -### 断言 -- 值检查**优先使用AssertJ**(`assertThat`)而非JUnit断言 -- 使用流式AssertJ API提高可读性 -- 异常: 使用JUnit `assertThrows`捕获,再用AssertJ验证消息 -- 成功路径: 使用JUnit `assertDoesNotThrow` - -### 事件驱动测试 -- 使用`AdviceWith`和`MockEndpoint`测试Camel路由 -- 验证消息内容、头部和路由逻辑 -- 单独测试错误处理路由 -- 单元测试中模拟外部系统(RabbitMQ、S3、数据库) - -### Quarkus特定 -- 保持最新的LTS版本(Quarkus 3.x) -- 使用Quarkus测试配置文件处理不同场景 -- 使用`@InjectMock`代替`@MockBean`(Quarkus特定) - -**请记住**: 保持测试快速、隔离和确定性。测试行为而非实现细节。 diff --git a/docs/zh-CN/skills/quarkus-verification/SKILL.md b/docs/zh-CN/skills/quarkus-verification/SKILL.md deleted file mode 100644 index 492c8a49..00000000 --- a/docs/zh-CN/skills/quarkus-verification/SKILL.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -name: quarkus-verification -description: "Quarkus项目验证循环:构建、静态分析、带覆盖率的测试、安全扫描、原生编译以及发布或PR前的diff审查。" -origin: ECC ---- - -# Quarkus 验证循环 - -在PR前、重大变更后和部署前运行。 - -## 何时激活 - -- 为Quarkus服务打开PR前 -- 大规模重构或依赖升级后 -- 预发布或生产的部署前验证 -- 运行完整的构建 → lint → 测试 → 安全扫描 → 原生编译流水线 -- 验证测试覆盖率达到阈值(80%+) -- 测试原生镜像兼容性 - -## 阶段1: 构建 - -```bash -# Maven -mvn clean verify -DskipTests - -# Gradle -./gradlew clean assemble -x test -``` - -构建失败时,停止并修复编译错误。 - -## 阶段2: 静态分析 - -### Checkstyle、PMD、SpotBugs(Maven) - -```bash -mvn checkstyle:check pmd:check spotbugs:check -``` - -### SonarQube(如已配置) - -```bash -mvn sonar:sonar \ - -Dsonar.projectKey=my-quarkus-project \ - -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.login=${SONAR_TOKEN} -``` - -### 需要解决的常见问题 - -- 未使用的导入或变量 -- 复杂方法(高圈复杂度) -- 潜在的空指针解引用 -- SpotBugs标记的安全问题 - -## 阶段3: 测试 + 覆盖率 - -```bash -# 运行所有测试 -mvn clean test - -# 生成覆盖率报告 -mvn jacoco:report - -# 强制覆盖率阈值(80%) -mvn jacoco:check - -# 或使用Gradle -./gradlew test jacocoTestReport jacocoTestCoverageVerification -``` - -### 测试类别 - -#### 单元测试 -使用模拟依赖测试服务逻辑: - -```java -@ExtendWith(MockitoExtension.class) -class UserServiceTest { - @Mock UserRepository userRepository; - @InjectMocks UserService userService; - - @Test - void createUser_validInput_returnsUser() { - var dto = new CreateUserDto("Alice", "alice@example.com"); - - // Panache的persist()返回void — 使用doNothing + verify - doNothing().when(userRepository).persist(any(User.class)); - - User result = userService.create(dto); - - assertThat(result.name).isEqualTo("Alice"); - verify(userRepository).persist(any(User.class)); - } -} -``` - -#### 集成测试 -使用真实数据库(Testcontainers)测试: - -```java -@QuarkusTest -@QuarkusTestResource(PostgresTestResource.class) -class UserRepositoryIntegrationTest { - - @Inject - UserRepository userRepository; - - @Test - @Transactional - void findByEmail_existingUser_returnsUser() { - User user = new User(); - user.name = "Alice"; - user.email = "alice@example.com"; - userRepository.persist(user); - - Optional found = userRepository.findByEmail("alice@example.com"); - - assertThat(found).isPresent(); - assertThat(found.get().name).isEqualTo("Alice"); - } -} -``` - -#### API测试 -使用REST Assured测试REST端点: - -```java -@QuarkusTest -class UserResourceTest { - - @Test - void createUser_validInput_returns201() { - given() - .contentType(ContentType.JSON) - .body(""" - {"name": "Alice", "email": "alice@example.com"} - """) - .when().post("/api/users") - .then() - .statusCode(201) - .body("name", equalTo("Alice")); - } - - @Test - void createUser_invalidEmail_returns400() { - given() - .contentType(ContentType.JSON) - .body(""" - {"name": "Alice", "email": "invalid"} - """) - .when().post("/api/users") - .then() - .statusCode(400); - } -} -``` - -### 覆盖率报告 - -检查`target/site/jacoco/index.html`获取详细覆盖率: -- 总体行覆盖率(目标: 80%+) -- 分支覆盖率(目标: 70%+) -- 识别未覆盖的关键路径 - -## 阶段4: 安全扫描 - -### 依赖漏洞(Maven) - -```bash -mvn org.owasp:dependency-check-maven:check -``` - -查看`target/dependency-check-report.html`中的CVE。 - -### Quarkus安全审计 - -```bash -# 检查有漏洞的扩展 -mvn quarkus:audit - -# 列出所有扩展 -mvn quarkus:list-extensions -``` - -### 常见安全检查 - -- [ ] 所有密钥在环境变量中(不在代码中) -- [ ] 所有端点有输入验证 -- [ ] 认证/授权已配置 -- [ ] CORS正确配置 -- [ ] 安全头已设置 -- [ ] 密码使用BCrypt哈希 -- [ ] SQL注入保护(参数化查询) -- [ ] 公共端点有速率限制 - -## 阶段5: 原生编译 - -测试GraalVM原生镜像兼容性: - -```bash -# 构建原生可执行文件 -mvn package -Dnative - -# 或使用容器 -mvn package -Dnative -Dquarkus.native.container-build=true - -# 测试原生可执行文件 -./target/*-runner - -# 运行基本冒烟测试 -curl http://localhost:8080/q/health/live -curl http://localhost:8080/q/health/ready -``` - -### 原生镜像故障排除 - -常见问题: -- **Reflection**: 为动态类添加反射配置 -- **Resources**: 使用`quarkus.native.resources.includes`包含资源 -- **JNI**: 使用原生库时注册JNI类 - -反射配置示例: -```java -@RegisterForReflection(targets = {MyDynamicClass.class}) -public class ReflectionConfiguration {} -``` - -## 阶段6: 健康检查 - -```bash -# 存活检查 -curl http://localhost:8080/q/health/live - -# 就绪检查 -curl http://localhost:8080/q/health/ready - -# 所有健康检查 -curl http://localhost:8080/q/health - -# 指标(如启用) -curl http://localhost:8080/q/metrics -``` - -## 验证清单 - -### 代码质量 -- [ ] 构建无警告通过 -- [ ] 静态分析干净(无高/中问题) -- [ ] 代码遵循团队规范 -- [ ] PR中无注释代码或TODO - -### 测试 -- [ ] 所有测试通过 -- [ ] 代码覆盖率 ≥ 80% -- [ ] 使用真实数据库的集成测试 -- [ ] 安全测试通过 -- [ ] 性能在可接受范围内 - -### 安全 -- [ ] 无依赖漏洞 -- [ ] 认证/授权已测试 -- [ ] 输入验证完成 -- [ ] 源代码中无密钥 -- [ ] 安全头已配置 - -### 部署 -- [ ] 原生编译成功 -- [ ] 容器镜像可构建 -- [ ] 健康检查正确响应 -- [ ] 目标环境配置有效 - -## 自动化验证脚本 - -```bash -#!/bin/bash -set -e - -echo "=== 阶段1: 构建 ===" -mvn clean verify -DskipTests - -echo "=== 阶段2: 静态分析 ===" -mvn checkstyle:check pmd:check spotbugs:check - -echo "=== 阶段3: 测试 + 覆盖率 ===" -mvn test jacoco:report jacoco:check - -echo "=== 阶段4: 安全扫描 ===" -mvn org.owasp:dependency-check-maven:check - -echo "=== 阶段5: 原生编译 ===" -mvn package -Dnative -Dquarkus.native.container-build=true - -echo "=== 所有阶段完成 ===" -echo "查看报告:" -echo " - 覆盖率: target/site/jacoco/index.html" -echo " - 安全: target/dependency-check-report.html" -echo " - 原生: target/*-runner" -``` - -## 最佳实践 - -- 每次PR前运行验证循环 -- 在CI/CD流水线中自动化 -- 立即修复问题,不积累技术债务 -- 保持覆盖率在80%以上 -- 定期更新依赖 -- 定期测试原生编译 -- 监控性能趋势 -- 记录破坏性变更 -- 审查安全扫描结果 -- 验证每个环境的配置