mcpField
J
java-spring-best-practices

java-spring-best-practices

github.com/cosbort開発539 回インストール13 週間前に更新
リポジトリ

java-spring-best-practices について

モダンな Java で Spring Boot アプリを開発する人向けのリファレンススキルです。Spring Boot 3.5.x/4.0.x と Java 17 以降を対象に、アーキテクチャ、検証、エラー処理、テスト、ログ、性能の実践方法を扱います。

主な機能

  1. 01レイヤー構成のアーキテクチャ
  2. 02Record による DTO と設定
  3. 03ProblemDetail によるエラー処理
  4. 04仮想スレッドと RestClient
  5. 05Mockito と Testcontainers によるテスト
  6. 06構造化ログとオブザーバビリティ

活用シーン

  1. 01Spring Boot サービス構成の設計
  2. 02API の検証とエラー応答の標準化
  3. 03実データベースを使った統合テスト
  4. 04Java 21 の仮想スレッドの適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cosbort/agent-skills --skill java-spring-best-practices

インストール後、Claude Code で/java-spring-best-practicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
references/
architecture.md
config-reference.md
exception-handling.md
performance-guide.md
streams-guide.md
testing-guide.md
scripts/
GlobalExceptionHandler.java
logback-config.xml
pom-template.xml
ProductController.java
ProductService.java
ProductServiceTest.java

詳細

Java Spring Boot ベストプラクティス v2.0

本番環境に対応した Spring Boot ガイドです。Spring Boot 3.5.x / 4.0.x、**Java 17 以降(Java 21 以降向けに最適化)**に対応しています。

v2.0 の新機能

  • DTO に Java Record を使用(デフォルトで不変)
  • 高い同時実行性が求められる I/O に仮想スレッドを使用(spring.threads.virtual.enabled=true)
  • 標準化されたエラーレスポンスに ProblemDetail(RFC 7807)を使用
  • RestTemplate に代わり RestClient を使用(流れるような API、同期処理)
  • Spring Boot 3.4 以降に構造化ログ機能を組み込み
  • 統合テストの主な戦略として Testcontainers を使用(H2 に代わるもの)
  • Micrometer Observation API によるオブザーバビリティ
  • 開発環境向けの Docker Compose 統合
  • 例外の階層構造に Sealed クラスを使用
  • GraalVM Native Image / CDS を考慮

クイックスタートチェックリスト

  • ✅ Java 17 以降(仮想スレッドには Java 21 以降)
  • ✅ 依存関係の管理に spring-boot-starter-parent を使用
  • ✅ @RequiredArgsConstructor を使ったコンストラクターインジェクション
  • ✅ DTO には Java Record を使用(可変な @Data クラスは使用しない)
  • ✅ コントローラーはルーティングのみ、サービスはビジネスロジックを担当
  • ✅ ProblemDetail(RFC 7807)+ @RestControllerAdvice
  • ✅ レコードコンポーネントに @Valid と Bean Validation を適用
  • ✅ SLF4J の {} プレースホルダーと構造化ログ
  • ✅ YAML プロファイル(dev/prod/test)
  • ✅ 単体テスト(Mockito)+ 統合テスト(Testcontainers)
  • ✅ spring.threads.virtual.enabled=true

コアアーキテクチャ

controller/   → HTTP routing only (thin)
    ↓
service/      → Business logic, caching, validation
    ↓
repository/   → Spring Data JPA
    ↓
entity/       → JPA domain models

各レイヤーは、自身より下位のレイヤーにのみ依存します。コントローラーがリポジトリーに直接アクセスすることはありません。レイヤーベースと機能ベースのパターンについては、architecture.md を参照してください。

依存性注入(コンストラクターのみ)
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository repository;
    private final ProductMapper mapper;
}

DTO としての Java Record

Record は @Data を使った Lombok クラスに代わるもので、不変で簡潔、かつスレッドセーフです。

// Request (with validation)
public record CreateProductRequest(
    @NotBlank @Size(min = 3, max = 100) String name,
    @NotNull @DecimalMin("0.01") BigDecimal price,
    @Size(max = 500) String description
) {}

// Response
public record ProductResponse(
    Long id, String name, BigDecimal price,
    String description, Instant createdAt
) {}
マッピング
@Component
public class ProductMapper {
    public ProductResponse toResponse(Product entity) {
        return new ProductResponse(entity.getId(), entity.getName(),
            entity.getPrice(), entity.getDescription(), entity.getCreatedAt());
    }

    public Product toEntity(CreateProductRequest req) {
        return Product.builder()
            .name(req.name()).price(req.price()).description(req.description()).build();
    }
}
Record を使用したコントローラー
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
public class ProductController {
    private final ProductService service;

    @PostMapping
    public ResponseEntity<ProductResponse> create(
            @Valid @RequestBody CreateProductRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(service.create(request));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getById(@PathVariable Long id) {
        return ResponseEntity.ok(service.getById(id));
    }

    @GetMapping
    public ResponseEntity<Page<ProductResponse>> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return ResponseEntity.ok(service.getAll(PageRequest.of(page, size)));
    }
}

完全な例については、ProductController.java と ProductService.java を参照してください。

例外処理(ProblemDetail + Sealed クラス)

RFC 7807 を有効にする
spring:
  mvc:
    problemdetails:
      enabled: true
Sealed 例外階層
public sealed abstract class ApplicationException extends RuntimeException
    permits ResourceNotFoundException, DuplicateResourceException,
            BusinessRuleException, ExternalServiceException {

    private final HttpStatus httpStatus;
    protected ApplicationException(String message, HttpStatus httpStatus) {
        super(message);
        this.httpStatus = httpStatus;
    }
    public HttpStatus getHttpStatus() { return httpStatus; }
}

public final class ResourceNotFoundException extends ApplicationException {
    public ResourceNotFoundException(String resource, Object id) {
        super("%s not found with id: %s".formatted(resource, id), HttpStatus.NOT_FOUND);
    }
}
ハンドラー
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ApplicationException.class)
    public ProblemDetail handleApp(ApplicationException ex) {
        log.warn("Application exception: {}", ex.getMessage());
        ProblemDetail p = ProblemDetail.forStatusAndDetail(ex.getHttpStatus(), ex.getMessage());
        p.setTitle(ex.getClass().getSimpleName());
        p.setProperty("timestamp", Instant.now());
        return p;
    }
}

レスポンス: {"type":"about:blank","title":"ResourceNotFoundException","status":404,"detail":"Product not found with id: 42","timestamp":"..."}

例外階層全体、バリデーションハンドラー、パターンについては、exception-handling.md を参照してください。

仮想スレッド(Java 21 以降)

プロパティを 1 つ設定するだけで、ブロッキング I/O(JDBC、REST 呼び出し)をリアクティブプログラミングなしでスケーラブルにできます。

spring:
  threads:
    virtual:
      enabled: true
  main:
    keep-alive: true

Tomcat、@Async、@Scheduled はすべて自動的に仮想スレッドを使用します。I/O 中の synchronized ブロックはピン留めの原因となるため避け、代わりに ReentrantLock を使用してください。

ピン留めの回避、コネクションプールのサイズ設定、検出方法については、performance-guide.md を参照してください。

RestClient(RestTemplate に代わるもの)

@Service
@RequiredArgsConstructor
public class ExternalApiService {
    private final RestClient restClient;

    public ProductResponse fetchProduct(Long id) {
        return restClient.get()
            .uri("/products/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
                throw new ResourceNotFoundException("External product", id);
            })
            .body(ProductResponse.class);
    }
}

宣言的インターフェースには @HttpExchange を使用してください。設定とタイムアウトについては、performance-guide.md を参照してください。

ロギング

@Slf4j
@Service
public class ProductService {
    public void process(Long id) {
        log.info("Processing product ID: {}", id);       // ✅ Placeholders
        log.error("Error processing product", exception); // ✅ Pass exception
    }
}
構造化ログ(Spring Boot 3.4 以降)
logging:
  structured:
    format:
      console: ecs    # Options: ecs, gelf, logstash

設定

spring:
  threads:
    virtual:
      enabled: true
  datasource:
    url: jdbc:postgresql://localhost:5432/db
    username: ${DB_USERNAME:dev}
    hikari:
      maximum-pool-size: 20
  jpa:
    hibernate.ddl-auto: validate
    open-in-view: false
  mvc:
    problemdetails:
      enabled: true
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=10m
Record ベースの設定プロパティ
@ConfigurationProperties(prefix = "app")
public record AppProperties(
    @NotBlank String name,
    @Valid JwtProperties jwt
) {
    public record JwtProperties(@NotBlank String secret, @NotNull Duration expiration) {}
}

プロファイル、Docker Compose、外部設定については、config-reference.md を参照してください。

テスト

単体テスト(Mockito + AssertJ)
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
    @Mock private ProductRepository repository;
    @Mock private ProductMapper mapper;
    @InjectMocks private ProductService service;

    @Test
    void shouldThrow_whenNotFound() {
        when(repository.findById(1L)).thenReturn(Optional.empty());
        assertThatThrownBy(() -> service.getById(1L))
            .isInstanceOf(ResourceNotFoundException.class);
    }
}
統合テスト(Testcontainers)
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class ProductControllerIT {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired private MockMvc mockMvc;

    @Test
    void shouldCreateProduct() throws Exception {
        mockMvc.perform(post("/api/v1/products")
                .contentType(APPLICATION_JSON)
                .content("""
                    {"name":"Widget","price":29.99,"description":"A widget"}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").value("Widget"));
    }
}

包括的なパターン、スライステスト、コンテナーの共有については、testing-guide.md を参照してください。

最新の Java イディオム

// .toList() instead of Collectors.toList() (Java 16+)
List<String> names = products.stream().map(Product::getName).toList();

// Pattern matching switch (Java 21+, exhaustive on sealed types)
return switch (ex) {
    case ResourceNotFoundException e -> "Missing: " + e.getMessage();
    case DuplicateResourceException e -> "Conflict: " + e.getMessage();
    case BusinessRuleException e -> "Rule: " + e.getMessage();
    case ExternalServiceException e -> "External: " + e.getMessage();
};

// Text blocks for JPQL
@Query("""
    SELECT p FROM Product p LEFT JOIN FETCH p.category
    WHERE p.price > :min ORDER BY p.createdAt DESC
    """)
List<Product> findExpensive(@Param("min") BigDecimal min);

高度なパターンについては、streams-guide.md を参照してください。

よくある落とし穴

間違い解決策
@Autowired フィールドインジェクションコンストラクターインジェクション(@RequiredArgsConstructor)
可変の @Data DTOJava Records
Collectors.toList().toList()(Java 16+)
統合テストに H2 を使用実際の DB を使う Testcontainers
独自のエラー JSON ラッパーProblemDetail(RFC 7807)
新規コードで RestTemplate を使用RestClient
synchronized と仮想スレッドの併用ReentrantLock
System.out.println()@Slf4j とプレースホルダー

参考ドキュメント

コード例

設計原則

  1. 不変性を優先 — DTO/設定にはレコードを使い、すべてのフィールドを final にする
  2. 単一責任 — 各クラスは一つのことだけを行う
  3. 早期に失敗させる — 入力を早期に検証し、意味のある例外をスローする
  4. 無効な状態を作れないようにする — シールド階層、強い型、検証
  5. デフォルトで可観測にする — 初日から構造化ログ、メトリクス、トレースを導入する

最終更新: March 2026 | 基準: Spring Boot 3.5.x / 4.0.x、Java 17+(21+ を推奨)

コメント