spring-boot について
本番向けサービスを構築する開発者を対象とした、Spring Boot 4.x開発スキルです。クリーンアーキテクチャ、セキュリティ、テスト、データアクセス、デプロイの指針を、具体的な規約やコードパターンとともに提供します。
主な機能
- 01レイヤードアーキテクチャとコンストラクターインジェクション
- 02RESTのバリデーションとProblem Details形式のエラー
- 03セキュリティとOAuth2/JWTの指針
- 04Flywayマイグレーションとデータアクセスのパターン
- 05ユニットテストと統合テストの手順
- 06Actuatorのヘルスチェック設定
活用シーン
- 01実装前のSpring Bootサービス設計
- 02検証とエラー処理を備えたRESTエンドポイントの構築
- 03セキュリティ機能とデータベースマイグレーションの追加
- 04デプロイ前のテストとヘルスチェックの確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ piomin/ claude-ai-spring-boot --skill spring-bootインストール後、Claude Code で/spring-bootを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cloud.mddata.mdsecurity.mdtesting.mdui-contract.mdweb.md詳細
Spring Boot スキル
クリーンアーキテクチャと本番環境に対応したコードを重視した、エンタープライズ向け Spring Boot 4.x 開発です。
基本的なワークフロー
- 分析 - 要件を理解し、サービスの境界、API、データモデルを特定します
- 設計 - アーキテクチャを計画し、コーディング前に設計を確認します
- 実装 - コンストラクターインジェクションとレイヤードアーキテクチャを使用して構築します。リレーショナルデータベースを使用する場合は、エンティティと並行して Flyway マイグレーションを作成します(
ddl-auto: validate) - セキュリティ保護 - Spring Security、OAuth2、メソッドセキュリティを追加し、テストが合格することを確認します
- テスト - 単体テストと統合テストを作成します。
./mvnw clean verifyを実行してすべて合格することを確認します。統合テストはスキップせずに実行します - デプロイ - Actuator でヘルスチェックを設定します。
/actuator/healthが UP を返し、/actuator/infoにbuild.versionが表示されることを確認します
クイックスタートテンプレート
エンティティ
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@DecimalMin("0.0")
private BigDecimal price;
// Getters/Setters (no Lombok)
}
リポジトリ
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContainingIgnoreCase(String name);
}
サービス
@Service
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
public List<ProductResponse> search(String name) {
return repo.findByNameContainingIgnoreCase(name).stream()
.map(ProductResponse::from)
.toList();
}
@Transactional
public ProductResponse create(ProductRequest request) {
var product = new Product();
product.setName(request.name());
product.setPrice(request.price());
return ProductResponse.from(repo.save(product));
}
}
REST コントローラー
@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public List<ProductResponse> search(@RequestParam(defaultValue = "") String name) {
return service.search(name);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse create(@Valid @RequestBody ProductRequest request) {
return service.create(request);
}
}
DTO(レコード)
public record ProductRequest(
@NotBlank String name,
@DecimalMin("0.0") BigDecimal price
) {}
public record ProductResponse(Long id, String name, BigDecimal price) {
public static ProductResponse from(Product product) {
return new ProductResponse(product.getId(), product.getName(), product.getPrice());
}
}
グローバル例外ハンドラー(Problem Details)
@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
public record FieldMessage(String field, String code, String message) {}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
ex.getBody().setProperty("errors", ex.getFieldErrors().stream()
.map(e -> new FieldMessage(e.getField(), code(e.getCode()), e.getDefaultMessage()))
.toList());
return super.handleMethodArgumentNotValid(ex, headers, status, request);
}
private static String code(String constraint) {
return switch (constraint) {
case "NotBlank", "NotNull" -> "required";
case null, default -> "invalid";
};
}
@ExceptionHandler
ProblemDetail handleOptimisticLock(ObjectOptimisticLockingFailureException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "Record was changed by someone else; reload and retry");
}
@ExceptionHandler
ProblemDetail handleUnexpected(Exception ex) {
logger.error("Unexpected error", ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected server error");
}
}
すべてのエラーは RFC 9457 Problem Details 形式で、フィールドエラーは errors: [{field, code, message}] に含めます。ハンドラーの完全版(一意制約、不正なソート、ドキュメント化されたコード)については、references/web.md を参照してください。
テストスライス
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean ProductService service;
@Test
void createProduct_validRequest_returns201() throws Exception {
var response = new ProductResponse(1L, "Widget", new BigDecimal("10.00"));
when(service.create(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name":"Widget","price":10.0}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Widget"));
}
}
リファレンスガイド
状況に応じて詳細なパターンを読み込みます。
| トピック | リファレンス | 読み込むタイミング |
|---|---|---|
| Web/REST | references/web.md | コントローラー、ページ分割されたリスト、バリデーション、PUT と version、errors[] を含む Problem Details(RFC 9457) |
| UI 向けに構築された API | references/ui-contract.md | UI または生成されたクライアントが OpenAPI コントラクトを利用する場合のみ(conventions を参照):/api、springdoc 3 のコントラクトエクスポート、エラーコード、フィルター、リレーション、Keycloak、「UI を接続する」 |
| データアクセス | references/data.md | JPA、リポジトリ、トランザクション、クエリ、Flyway マイグレーション |
| セキュリティ | references/security.md | Spring Security 6、OAuth2、JWT、認証 |
| クラウド/設定 | references/cloud.md | Config Server、サービスディスカバリー、レジリエンス、仮想スレッド、HTTP インターフェイス、GraalVM ネイティブ、OTLP トレーシング |
| テスト | references/testing.md | 単体テスト、統合テスト、スライステスト、Testcontainers @ServiceConnection |
制約
必ず行うこと
- コンストラクターインジェクションを使用する(フィールドインジェクションは禁止)
- すべてのリクエストボディに
@Validを付ける - 複数ステップの書き込みには
@Transactionalを使用する - 読み取りには
@Transactional(readOnly = true)を使用する @ConfigurationPropertiesによる型安全な設定を使用する@RestControllerAdviceによるグローバル例外処理を行う- シークレットを外部化する(プロパティファイルではなく環境変数を使用する)
- リレーショナルデータベースにデータを永続化する場合は Flyway マイグレーションと
ddl-auto: validateを使用する(それ以外の場合は Flyway を使用しない) - エラーは Problem Details 形式とし、リストエンドポイントは
PagedModelを返す - セキュリティの背後でライブネスプローブとレディネスプローブが機能するよう、
/actuator/health/**を許可する - UI 向けに API を構築する場合のみ(
conventionsの「UI 向けに構築された REST API」を参照)、references/ui-contract.mdに従う
必ず避けること
- フィールドインジェクション(フィールドへの
@Autowired) - エンドポイントの入力バリデーションを省略する
- ブロッキングコードとリアクティブコードを混在させる
- シークレットを application.properties に保存する
- 非推奨の Spring Boot 2.x/3.x パターンを使用する
- URL、認証情報、環境値をハードコードする
- テスト以外で
ddl-auto: update/createを使用する、または適用済みのマイグレーションを編集する - コントローラーから
Page/PageImplを返す、またはアプリケーションクラスに@EnableSpringDataWebSupportを付ける(@WebMvcTestで二重に登録されます) - コントローラーメソッドに
@ApiResponseを付ける(springdoc 3 では成功レスポンスが削除されます。OpenApiCustomizerを使用してください) - キャッチオールハンドラーから例外のメッセージを返す
Spring Boot 4 / Spring Data 4 に関する注意事項
PropertyReferenceExceptionとTypeInformationはorg.springframework.data.coreに移動しました@MockBean/@SpyBeanは削除されました。@MockitoBean/@MockitoSpyBeanを使用してください@WebMvcTestはリポジトリを作成しません。コントローラーが注入するすべてのリポジトリに対して、テスト内で@MockitoBeanが必要です./mvnw clean verifyでビルドしてください。target内に IDE がコンパイルしたクラスがあると、javacのエラーが隠れることがあります
アーキテクチャパターン
プロジェクト構成:
src/main/java/pl/piomin/services/
├── controller/ # REST endpoints
├── service/ # Business logic
├── repository/ # Data access
├── model/ # Entities
├── dto/ # Request/Response DTOs
├── config/ # Configuration
└── exception/ # Custom exceptions + handler
レイヤー構成:
- コントローラー → サービス → リポジトリ
- コントローラーは HTTP とバリデーションを担当します
- サービスはビジネスロジックとトランザクションを担当します
- リポジトリはデータ永続化を担当します
クリーンアーキテクチャの原則:
- ドメインモデルはフレームワークから独立させます
- ユースケース駆動の設計
- 依存性逆転(インターフェース)
- レイヤー間の境界を明確にします
よく使われるアノテーション
| アノテーション | 用途 |
|---|---|
@RestController | REST コントローラー(@Controller と @ResponseBody を組み合わせたもの) |
@Service | ビジネスロジックコンポーネント |
@Repository | データアクセスコンポーネント |
@Transactional | トランザクション管理 |
@Valid | バリデーションを実行 |
@ConfigurationProperties | プロパティをクラスにバインド |
@EnableMethodSecurity | メソッドセキュリティを有効化 |
リアクティブ WebFlux エンドポイント
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<OrderDto>> getOrder(@PathVariable UUID id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<OrderDto> createOrder(@Valid @RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
}
Spring Security JWT
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll() // + "/actuator/info" when built for a UI
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}
ナレッジベース
Spring Boot 4.x、Java 21+、Spring WebFlux、Project Reactor、Spring Data JPA、Spring Security、OAuth2/JWT、Hibernate、R2DBC、Spring Cloud、Resilience4j、Micrometer、JUnit 5、Testcontainers、Mockito、Maven、Paketo Buildpacks、Skaffold
コメント
Markdown 対応