java-spring-best-practices について
モダンな Java で Spring Boot アプリを開発する人向けのリファレンススキルです。Spring Boot 3.5.x/4.0.x と Java 17 以降を対象に、アーキテクチャ、検証、エラー処理、テスト、ログ、性能の実践方法を扱います。
主な機能
- 01レイヤー構成のアーキテクチャ
- 02Record による DTO と設定
- 03ProblemDetail によるエラー処理
- 04仮想スレッドと RestClient
- 05Mockito と Testcontainers によるテスト
- 06構造化ログとオブザーバビリティ
活用シーン
- 01Spring Boot サービス構成の設計
- 02API の検証とエラー応答の標準化
- 03実データベースを使った統合テスト
- 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.mdreferences/architecture.mdconfig-reference.mdexception-handling.mdperformance-guide.mdstreams-guide.mdtesting-guide.mdscripts/GlobalExceptionHandler.javalogback-config.xmlpom-template.xmlProductController.javaProductService.javaProductServiceTest.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 DTO | Java Records |
Collectors.toList() | .toList()(Java 16+) |
| 統合テストに H2 を使用 | 実際の DB を使う Testcontainers |
| 独自のエラー JSON ラッパー | ProblemDetail(RFC 7807) |
| 新規コードで RestTemplate を使用 | RestClient |
synchronized と仮想スレッドの併用 | ReentrantLock |
System.out.println() | @Slf4j とプレースホルダー |
参考ドキュメント
- アーキテクチャ — レイヤーベースと機能ベースの構成
- 例外処理 — シールド階層、ProblemDetail
- 設定 — プロファイル、Docker Compose、レコード
- Streams と Java のイディオム — 関数型パターン、最新の構文
- テスト — Mockito、Testcontainers、スライステスト
- パフォーマンス — 仮想スレッド、キャッシュ、可観測性
コード例
- ProductController.java — レコードを使った REST コントローラー
- ProductService.java — ビジネスロジックを含むサービス
- GlobalExceptionHandler.java — ProblemDetail ハンドラー
- ProductServiceTest.java — 単体テストと IT のパターン
- pom-template.xml — Maven の完全なセットアップ
設計原則
- 不変性を優先 — DTO/設定にはレコードを使い、すべてのフィールドを final にする
- 単一責任 — 各クラスは一つのことだけを行う
- 早期に失敗させる — 入力を早期に検証し、意味のある例外をスローする
- 無効な状態を作れないようにする — シールド階層、強い型、検証
- デフォルトで可観測にする — 初日から構造化ログ、メトリクス、トレースを導入する
最終更新: March 2026 | 基準: Spring Boot 3.5.x / 4.0.x、Java 17+(21+ を推奨)