Skip to content
Published on

Spring Boot 3.x 完全ガイド 2025: Virtual Threads、GraalVM Native、Spring AI

Authors

はじめに

Spring Boot 3.xは、Javaエコシステムにおいて最も重要なフレームワークアップデートです。Jakarta EE 10への移行、Java 17+ベースライン、Java 21 Virtual Threadsのネイティブ統合により、エンタープライズバックエンド開発の新たな標準を提示しています。

本ガイドでは、Virtual ThreadsとGraalVM Native ImageからSpring AI、Security 6、Testcontainers、Observabilityまで、2025年にプロダクションレディなアプリケーションを構築するために必要な全てをカバーします。


1. Spring Boot 3.x ビッグピクチャー

1.1 Jakarta EE マイグレーション

Spring Boot 3.xで最も大きな破壊的変更は、javaxからjakartaへの名前空間の移行です。

コンポーネント変更前 (2.x)変更後 (3.x)
Servletjavax.servletjakarta.servlet
JPAjavax.persistencejakarta.persistence
Validationjavax.validationjakarta.validation
Mailjavax.mailjakarta.mail
Injectjavax.injectjakarta.inject
// 変更前: Spring Boot 2.x
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

// 変更後: Spring Boot 3.x
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;

OpenRewriteによる自動マイグレーション:

# build.gradle
plugins {
    id("org.openrewrite.rewrite") version "6.24.0"
}

rewrite {
    activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3")
}

# マイグレーション実行
./gradlew rewriteRun

1.2 Java 21 ベースライン

Spring Boot 3.xはJava 17を最低要件としますが、Java 21に完全最適化されています:

// Spring Boot 3.xで使用可能なJava 21機能

// 1. レコードパターン
public ResponseEntity<?> processPayment(Object payment) {
    return switch (payment) {
        case CreditCardPayment(var card, var amount)
            -> processCard(card, amount);
        case BankTransfer(var account, var amount)
            -> processTransfer(account, amount);
        default -> ResponseEntity.badRequest().build();
    };
}

// 2. Sealedインターフェースによるドメインモデリング
public sealed interface PaymentResult
    permits PaymentSuccess, PaymentFailure, PaymentPending {
}
public record PaymentSuccess(String transactionId) implements PaymentResult {}
public record PaymentFailure(String reason) implements PaymentResult {}
public record PaymentPending(String retryAfter) implements PaymentResult {}

1.3 推奨モジュール構造

my-app/
  app-api/           # RESTコントローラー、DTO
  app-domain/        # エンティティ、リポジトリ、サービス
  app-infra/         # 外部連携、設定
  app-common/        # 共通ユーティリティ
  build.gradle       # ルートビルド設定

2. Java 21 Virtual Threads 実践活用

2.1 Virtual Threads vs OSスレッド

Virtual ThreadsはOSではなくJVMが管理する軽量スレッドです。従来のプラットフォームスレッドのメモリオーバーヘッドなしに、大規模な同時実行を実現します。

// プラットフォームスレッド: OSスレッドと1:1マッピング、各約1MBスタック
// 10,000同時スレッド -> 10GBメモリ

// Virtual Thread: JVM管理、各約1KBスタック
// 1,000,000同時スレッド -> 約1GBメモリ

// Virtual Threadの作成
Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread.currentThread());
});

// Virtual Thread用Executor
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

2.2 Spring BootでのVirtual Threads有効化

1行で設定完了です:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

これにより、Tomcatの全リクエスト処理がVirtual Threadで実行されます。

2.3 ベンチマーク: 従来スレッド vs Virtual Threads

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class VirtualThreadBenchmarkTest {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void benchmark10kConcurrentRequests() throws Exception {
        int concurrency = 10_000;
        CountDownLatch latch = new CountDownLatch(concurrency);
        AtomicInteger success = new AtomicInteger(0);

        long start = System.nanoTime();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < concurrency; i++) {
                executor.submit(() -> {
                    try {
                        ResponseEntity<String> response =
                            restTemplate.getForEntity("/api/slow-endpoint", String.class);
                        if (response.getStatusCode().is2xxSuccessful()) {
                            success.incrementAndGet();
                        }
                    } finally {
                        latch.countDown();
                    }
                });
            }
            latch.await(60, TimeUnit.SECONDS);
        }

        long elapsed = Duration.ofNanos(System.nanoTime() - start).toMillis();
        System.out.printf("10Kリクエスト: %dms, 成功: %d%n", elapsed, success.get());
        // Virtual Threads: 100ms DB呼び出しで約2,500ms
        // プラットフォームスレッド (200プール): 約50,000ms
    }
}

2.4 Virtual Threads vs WebFlux

// === リアクティブ方式 (WebFlux) ===
@GetMapping("/users-reactive")
public Mono<List<User>> getUsersReactive() {
    return webClient.get()
        .uri("/api/users")
        .retrieve()
        .bodyToFlux(User.class)
        .collectList()
        .flatMap(users ->
            externalService.enrich(users)
                .map(enriched -> enriched)
        );
}

// === Virtual Thread方式 (同期コード、非同期性能) ===
@GetMapping("/users-virtual")
public List<User> getUsersVirtual() {
    List<User> users = restClient.get()
        .uri("/api/users")
        .retrieve()
        .body(new ParameterizedTypeReference<>() {});

    return externalService.enrich(users); // ブロッキングOK!
}

重要ポイント: Virtual Threadsを使えば、同期コードを書きながらReactive Programmingに匹敵するスループットを達成できます。デバッグも劇的に簡単になります。

2.5 Virtual Threadsの注意点

// 1. synchronizedは使わない -> ReentrantLockを使用
// NG: Virtual Threadがキャリアスレッドにピン留めされる
public synchronized void updateCounter() {
    counter++;
}

// OK: ReentrantLockはyieldを許可する
private final ReentrantLock lock = new ReentrantLock();
public void updateCounter() {
    lock.lock();
    try {
        counter++;
    } finally {
        lock.unlock();
    }
}

// 2. ThreadLocalの使用を最小化
// Virtual Threadは数百万個生成可能
// ThreadLocal乱用はメモリ爆発の原因
// -> ScopedValue (Preview) の使用を推奨

// 3. CPU負荷の高い処理はプラットフォームスレッドが有利
@Bean
TaskExecutor cpuIntensiveExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
    executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
    return executor;
}

// 4. ピン留め検出用JVMフラグ
// -Djdk.tracePinnedThreads=short

3. GraalVM Native Image

3.1 なぜNative Imageか?

GraalVM Native ImageはSpring BootアプリをAOT(事前コンパイル)でネイティブバイナリに変換します。その結果は驚異的です:

指標JVMモードNative Image
起動時間2-5秒0.05-0.2秒
メモリ (RSS)200-500MB30-80MB
ピークスループット高い中〜高
ビルド時間速い3-10分
リフレクション制約なしヒント必要

3.2 Native Imageビルド設定

// build.gradle
plugins {
    id 'org.graalvm.buildtools.native' version '0.10.3'
}

graalvmNative {
    binaries {
        main {
            imageName = 'my-app'
            mainClass = 'com.example.MyApplication'
            buildArgs.addAll(
                '--no-fallback',
                '-H:+ReportExceptionStackTraces',
                '--enable-url-protocols=http,https'
            )
        }
    }
}
# Native Imageビルド
./gradlew nativeCompile

# 実行
./build/native/nativeCompile/my-app
# 0.087秒で起動!

3.3 リフレクションヒント

@Configuration
@ImportRuntimeHints(AppRuntimeHints.class)
public class NativeImageConfig {
}

public class AppRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // リフレクション対象の登録
        hints.reflection()
            .registerType(UserDto.class, MemberCategory.values())
            .registerType(OrderDto.class, MemberCategory.values());

        // リソースの登録
        hints.resources()
            .registerPattern("templates/*")
            .registerPattern("static/**")
            .registerPattern("messages/*.properties");

        // JDKプロキシの登録
        hints.proxies()
            .registerJdkProxy(MyRepository.class);

        // シリアライゼーションの登録
        hints.serialization()
            .registerType(MyEvent.class);
    }
}

// より簡単な方法: アノテーションベース
@RegisterReflectionForBinding({UserDto.class, OrderDto.class})
@Configuration
public class ReflectionConfig {
}

3.4 Dockerマルチステージ Nativeビルド

# ステージ1: Native Imageビルド
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew nativeCompile --no-daemon

# ステージ2: 最小ランタイム
FROM debian:bookworm-slim
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring
COPY --from=builder /app/build/native/nativeCompile/my-app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
docker build -t my-app:native .
docker run -p 8080:8080 my-app:native
# 0.071秒で起動、42MB RSS

3.5 Native Imageの制限事項と対策

// 問題: 動的クラスローディングが動作しない
// 解決策: ビルド時に登録する

// 問題: リフレクションを多用するライブラリ (Jackson, Hibernate)
// 解決策: Spring AOTが大部分のヒントを自動生成

// 問題: ビルド時間が長い (5-10分)
// 解決策: 開発時はJVMモード、本番はNativeで
// CI/CDではCloud Native Buildpacksを使用:
// ./gradlew bootBuildImage --imageName=my-app:native

4. Spring AI — LLM統合

4.1 Spring AI 概要

Spring AIは、複数のLLMプロバイダー(OpenAI、Anthropic、Ollama等)に対して、馴染みのあるSpringパターンで一貫した抽象化を提供します。

dependencies {
    // プロバイダーを選択
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
    // implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
    // implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'

    // RAG用ベクトルストア
    implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter'
}
# application.yml
spring:
  ai:
    openai:
      api-key: your-api-key-here
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

4.2 ChatClientの使用方法

@RestController
@RequiredArgsConstructor
public class AiController {

    private final ChatClient.Builder chatClientBuilder;

    // シンプルチャット
    @GetMapping("/ai/chat")
    public String chat(@RequestParam String message) {
        return chatClientBuilder.build()
            .prompt()
            .user(message)
            .call()
            .content();
    }

    // ストリーミングレスポンス
    @GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam String message) {
        return chatClientBuilder.build()
            .prompt()
            .user(message)
            .stream()
            .content();
    }

    // 構造化出力
    @GetMapping("/ai/analyze")
    public SentimentResult analyzeSentiment(@RequestParam String text) {
        return chatClientBuilder.build()
            .prompt()
            .user("Analyze the sentiment of this text: " + text)
            .call()
            .entity(SentimentResult.class);
    }
}

record SentimentResult(String sentiment, double confidence, String explanation) {}

4.3 RAG (検索拡張生成)

@Service
@RequiredArgsConstructor
public class RagService {

    private final ChatClient.Builder chatClientBuilder;
    private final VectorStore vectorStore;

    // ドキュメントをベクトルストアに格納
    public void ingestDocuments(List<Document> documents) {
        vectorStore.add(documents);
    }

    // RAGコンテキスト付きクエリ
    public String queryWithContext(String question) {
        // 1. 類似度検索で関連ドキュメントを取得
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(5)
        );

        // 2. 結果からコンテキストを構築
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));

        // 3. コンテキスト付きでLLMに問い合わせ
        return chatClientBuilder.build()
            .prompt()
            .system(s -> s.text("""
                あなたは親切なアシスタントです。提供されたコンテキストに基づいて回答してください。
                コンテキストに回答がない場合は、分からないと答えてください。

                コンテキスト:
                {context}
                """).param("context", context))
            .user(question)
            .call()
            .content();
    }
}

4.4 Function Calling (Tool Calling)

@Configuration
public class AiFunctionConfig {

    @Bean
    @Description("指定された都市の現在の天気を取得")
    public Function<WeatherRequest, WeatherResponse> currentWeather() {
        return request -> {
            // 実際の天気APIを呼び出し
            return new WeatherResponse(request.city(), 25.0, "晴れ");
        };
    }

    @Bean
    @Description("カタログから商品を検索")
    public Function<ProductSearchRequest, List<Product>> searchProducts(
            ProductRepository productRepository) {
        return request -> productRepository
            .findByNameContaining(request.query());
    }
}

record WeatherRequest(String city) {}
record WeatherResponse(String city, double temperature, String condition) {}
record ProductSearchRequest(String query) {}

// 使用例: LLMが自動的に登録された関数を呼び出す
@GetMapping("/ai/assistant")
public String assistant(@RequestParam String question) {
    return chatClientBuilder.build()
        .prompt()
        .user(question) // "東京の天気は?"
        .functions("currentWeather", "searchProducts")
        .call()
        .content();
}

5. Spring Security 6

5.1 SecurityFilterChain 設定

Spring Security 6はLambda DSLを専用とし、WebSecurityConfigurerAdapterを廃止しました。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/posts/**").authenticated()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(
                    new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            )
            .build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder
            .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .build();
    }
}

5.2 メソッドセキュリティ

@Configuration
@EnableMethodSecurity // @EnableGlobalMethodSecurityの代替
public class MethodSecurityConfig {}

@Service
public class PostService {

    @PreAuthorize("hasRole('ADMIN') or #authorId == authentication.principal.id")
    public void deletePost(Long postId, Long authorId) {
        // 管理者または作成者のみ削除可能
    }

    @PostAuthorize("returnObject.author.id == authentication.principal.id")
    public Post getMyPost(Long postId) {
        return postRepository.findById(postId).orElseThrow();
    }
}

5.3 本番環境向けセキュリティヘッダー

@Bean
@Profile("prod")
public SecurityFilterChain productionSecurity(HttpSecurity http) throws Exception {
    return http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'"))
            .frameOptions(frame -> frame.deny())
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000))
            .permissionsPolicy(pp -> pp
                .policy("camera=(), microphone=(), geolocation=()"))
        )
        .build();
}

6. データアクセス: JPA、R2DBC、QueryDSL

6.1 Spring Data JPA with Hibernate 6

@Entity
@Table(name = "posts")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Enumerated(EnumType.STRING)
    private PostStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private User author;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // ビジネスメソッド
    public void publish() {
        if (this.status != PostStatus.DRAFT) {
            throw new IllegalStateException("下書き状態の投稿のみ公開できます");
        }
        this.status = PostStatus.PUBLISHED;
    }
}

6.2 N+1問題の解決

public interface PostRepository extends JpaRepository<Post, Long> {

    // 解決策1: EntityGraph
    @EntityGraph(attributePaths = {"author", "comments"})
    List<Post> findByStatus(PostStatus status);

    // 解決策2: Fetch Join
    @Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.author WHERE p.status = :status")
    List<Post> findByStatusWithAuthor(@Param("status") PostStatus status);

    // 解決策3: Batch Size (application.ymlでグローバル設定)
    // spring.jpa.properties.hibernate.default_batch_fetch_size: 100
}

6.3 QueryDSL統合

@Repository
@RequiredArgsConstructor
public class PostQueryRepository {

    private final JPAQueryFactory queryFactory;

    public Page<PostDto> searchPosts(PostSearchCondition condition, Pageable pageable) {
        QPost post = QPost.post;
        QUser user = QUser.user;

        List<PostDto> content = queryFactory
            .select(Projections.constructor(PostDto.class,
                post.id, post.title, post.status,
                user.name, post.createdAt))
            .from(post)
            .join(post.author, user)
            .where(
                titleContains(condition.title()),
                statusEquals(condition.status()),
                createdAfter(condition.fromDate())
            )
            .orderBy(post.createdAt.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        Long total = queryFactory
            .select(post.count())
            .from(post)
            .where(
                titleContains(condition.title()),
                statusEquals(condition.status()),
                createdAfter(condition.fromDate())
            )
            .fetchOne();

        return new PageImpl<>(content, pageable, total != null ? total : 0);
    }

    private BooleanExpression titleContains(String title) {
        return title != null ? QPost.post.title.contains(title) : null;
    }

    private BooleanExpression statusEquals(PostStatus status) {
        return status != null ? QPost.post.status.eq(status) : null;
    }

    private BooleanExpression createdAfter(LocalDateTime from) {
        return from != null ? QPost.post.createdAt.after(from) : null;
    }
}

6.4 R2DBC (リアクティブデータアクセス)

public interface PostR2dbcRepository extends ReactiveCrudRepository<Post, Long> {

    Flux<Post> findByStatus(PostStatus status);

    @Query("SELECT * FROM posts WHERE title LIKE :keyword ORDER BY created_at DESC LIMIT :limit")
    Flux<Post> searchByTitle(String keyword, int limit);
}

7. Testcontainers統合テスト

7.1 ServiceConnection自動設定

@SpringBootTest
@Testcontainers
class PostServiceIntegrationTest {

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

    @Container
    @ServiceConnection
    static GenericContainer<?> redis =
        new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379);

    @Autowired
    private PostService postService;

    @Test
    void shouldCreateAndRetrievePost() {
        CreatePostRequest request = new CreatePostRequest("タイトル", "コンテンツ");
        Post created = postService.createPost(request);

        Post found = postService.getPost(created.getId());
        assertThat(found.getTitle()).isEqualTo("タイトル");
        assertThat(found.getStatus()).isEqualTo(PostStatus.DRAFT);
    }
}

7.2 テストコンテナの再利用

@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine")
            .withReuse(true);
    }

    @Bean
    @ServiceConnection(name = "redis")
    GenericContainer<?> redisContainer() {
        return new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379)
            .withReuse(true);
    }
}

// テストコンテナで開発サーバーを実行
public class TestMyApplication {
    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main)
            .with(TestcontainersConfig.class)
            .run(args);
    }
}

7.3 Kafka Testcontainer

@SpringBootTest
@Testcontainers
class KafkaIntegrationTest {

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
    );

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Test
    void shouldProcessOrderEvent() throws Exception {
        OrderEvent event = new OrderEvent("order-123", "CREATED", BigDecimal.valueOf(99.99));

        kafkaTemplate.send("orders", event).get();

        await().atMost(Duration.ofSeconds(10))
            .untilAsserted(() ->
                assertThat(consumer.getProcessedOrders())
                    .hasSize(1)
                    .first()
                    .extracting(OrderEvent::orderId)
                    .isEqualTo("order-123")
            );
    }
}

8. Observability: Micrometer + OpenTelemetry

8.1 OTLP設定

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
    tags:
      application: my-spring-app
  tracing:
    sampling:
      probability: 1.0
  otlp:
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-tracing-bridge-otel'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
}

8.2 カスタムメトリクス

@Service
public class OrderService {

    private final Counter orderCounter;
    private final Timer orderProcessingTimer;
    private final MeterRegistry meterRegistry;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.orderCounter = Counter.builder("orders.created")
            .description("作成された注文の合計")
            .register(meterRegistry);
        this.orderProcessingTimer = Timer.builder("orders.processing.time")
            .description("注文処理時間")
            .register(meterRegistry);
    }

    public Order createOrder(CreateOrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(request);
            orderCounter.increment();
            meterRegistry.gauge("orders.pending",
                orderRepository.countByStatus(OrderStatus.PENDING));
            return order;
        });
    }
}

8.3 分散トレーシング

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    private final ObservationRegistry observationRegistry;

    @PostMapping("/api/orders")
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        return Observation.createNotStarted("order.creation", observationRegistry)
            .lowCardinalityKeyValue("order.type", request.type())
            .observe(() -> {
                Order order = orderService.createOrder(request);
                return ResponseEntity.created(
                    URI.create("/api/orders/" + order.getId())
                ).body(order);
            });
    }
}

8.4 Grafanaスタック (Docker Compose)

# docker-compose-observability.yml
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    ports:
      - "4317:4317"
      - "4318:4318"
    volumes:
      - ./otel-config.yaml:/etc/otelcol-contrib/config.yaml

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

  tempo:
    image: grafana/tempo:latest
    ports:
      - "3200:3200"
    command: [ "-config.file=/etc/tempo.yaml" ]

9. DockerとKubernetesデプロイ

9.1 最適化されたLayered Dockerfile

# Layered JARで最適なDockerキャッシュを実現
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

9.2 Cloud Native Buildpacks

# Dockerfileが不要!
./gradlew bootBuildImage --imageName=myregistry/my-app:latest

# Native Image用
./gradlew bootBuildImage \
  --imageName=myregistry/my-app:native \
  --builder=paketobuildpacks/builder-jammy-tiny

9.3 Kubernetesマニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spring-app
  template:
    metadata:
      labels:
        app: spring-app
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      containers:
        - name: spring-app
          image: myregistry/spring-app:latest
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
            - name: JAVA_OPTS
              value: "-XX:MaxRAMPercentage=75.0 -XX:+UseZGC"
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            failureThreshold: 30
            periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
  name: spring-app-service
spec:
  selector:
    app: spring-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spring-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spring-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

9.4 Graceful Shutdown

# application-prod.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
@Component
@Slf4j
public class GracefulShutdownHandler {

    @PreDestroy
    public void onShutdown() {
        log.info("グレースフルシャットダウン中...");
        // 処理中のリクエストを完了
        // コネクションプールを閉じる
        // メトリクスをフラッシュ
    }
}

10. プロダクションチェックリスト

セキュリティ

  • ブラウザベースAPIのCSRF保護
  • CORSを許可されたオリジンに制限
  • セキュリティヘッダー (CSP、HSTS、X-Frame-Options)
  • シークレットを環境変数またはVaultで外部化
  • Actuatorエンドポイントのセキュリティ制限

パフォーマンス

  • I/O負荷の高いワークロードでVirtual Threadsを有効化
  • HikariCPプールサイズの調整 (CPUコア数 x 2 + ディスクスピンドル)
  • JPA open-in-viewをfalseに設定
  • バッチフェッチサイズの設定 (100)
  • コネクションタイムアウトの設定 (3秒)

信頼性

  • Graceful Shutdownの有効化
  • ヘルスプローブの設定 (readiness + liveness)
  • Resilience4jによるサーキットブレーカー
  • 指数バックオフ付きリトライ
  • FlywayによるDBマイグレーション管理

運用

  • 構造化JSONロギング
  • OTLPによる分散トレーシング
  • Prometheusメトリクスの公開
  • アラートルールの設定
  • Renovate/Dependabotによる定期的な依存関係更新

本番用 application.yml

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        default_batch_fetch_size: 100
        jdbc.batch_size: 50
        order_inserts: true
        order_updates: true
  threads:
    virtual:
      enabled: true

server:
  shutdown: graceful
  tomcat:
    max-connections: 10000
    accept-count: 200
    connection-timeout: 5s

logging:
  level:
    root: WARN
    com.myapp: INFO
  pattern:
    console: "%d [%thread] %-5level %logger - traceId=%X{traceId} - %msg%n"

11. インタビュー質問 15選

基礎 (1-5)

Q1. Spring Boot 3.xの主要な破壊的変更は?

Jakarta EEマイグレーション(javaxからjakarta)、Java 17最低要件、WebSecurityConfigurerAdapterの廃止、Spring Security 6 Lambda DSL、requestMatchersがantMatchersを代替。

Q2. Virtual Threadsとリアクティブプログラミングの違いは?

Virtual Threadsは同期プログラミングモデルを維持しながら、JVM管理の軽量スレッドで高い同時実行を実現します。Reactiveは非同期ノンブロッキングモデルとオペレータを使用します。Virtual ThreadsはI/Oバウンド処理に優れ、Reactiveはバックプレッシャーを伴うストリーミングに優れています。

Q3. GraalVM Native Imageの制限事項は?

リフレクション、動的プロキシ、JNIはRuntimeHintsによる明示的な設定が必要です。ビルド時間が長く(5-10分)、JIT最適化がないためピークスループットはJVMより低い場合があります。互換性のないライブラリも存在します。

Q4. WebSecurityConfigurerAdapterが廃止された理由は?

SecurityFilterChain Beanを使用したコンポーネントベースのセキュリティ設定を可能にするためです。より柔軟で、テスト可能で、異なるURLパターンに対して複数のセキュリティフィルターチェーンを許可します。

Q5. TestcontainersのServiceConnectionの仕組みは?

コンテナタイプを自動検出し、動的ポートとホスト情報をSpring Boot auto-configurationプロパティに自動マッピングします。手動のDynamicPropertySource設定が不要になります。

中級 (6-10)

Q6. Virtual Threadsのスレッドピン留めとは?

Virtual Threadがsynchronizedブロックに入るか、ネイティブコールを実行すると、キャリアプラットフォームスレッドにピン留めされ、他のVirtual Threadがそのキャリアを使用できなくなります。synchronizedの代わりにReentrantLockを使用してください。

Q7. Spring AIのRAGパターンを説明してください。

ドキュメントをベクトルに変換してVectorStoreに格納します。クエリ時に類似度検索で関連ドキュメントを取得し、ユーザーの質問と共にLLMプロンプトにコンテキストとして渡すことで、事実に基づいた回答を生成します。

Q8. JPAのN+1問題を解決する3つの方法は?

EntityGraph(宣言的)、Fetch Join(JPQLで制御)、Batch Size(遅延読み込みをIN句でグループ化するグローバル設定)です。それぞれ柔軟性とクエリの複雑さにトレードオフがあります。

Q9. MicrometerとOpenTelemetryの関係は?

Micrometerはメトリクス抽象化レイヤーです。OpenTelemetryはオブザーバビリティ標準(トレース、メトリクス、ログ)です。Spring Boot 3.xはMicrometer Tracingを介して両者を橋渡しし、OTLP互換バックエンドにエクスポートします。

Q10. open-in-viewをfalseに設定すべき理由は?

true(デフォルト)の場合、永続性コンテキストがビューレイヤーまで開いたままになり、レンダリング中に遅延読み込みクエリが発生し、データベースコネクション使用時間が延長されます。APIサーバーでは必ずfalseに設定すべきです。

上級 (11-15)

Q11. Spring BootのAOT処理の仕組みは?

ビルド時にSpringがBean定義を分析し、プロキシクラスを生成し、リフレクションメタデータを収集し、最適化されたBeanファクトリーコードを作成します。GraalVM Native Imageの前提条件であり、JVMモードの起動時間も短縮します。

Q12. readinessプローブとlivenessプローブの違いは?

Readinessプローブ(actuator/health/readiness)はアプリがトラフィックを受信できるか確認します。失敗するとServiceから除外されます。Livenessプローブ(actuator/health/liveness)はアプリが生存しているか確認します。失敗するとPodが再起動されます。

Q13. HikariCPコネクションプールの最適化方法は?

maximum-pool-sizeをCPUコア数 x 2 + ディスクスピンドルに設定します。connection-timeoutは3秒以内に。idle-timeoutは10分、max-lifetimeはデータベースのwait_timeoutより短く設定します。

Q14. Kubernetes環境でのGraceful Shutdownを説明してください。

SIGTERMを受信すると、Springは新しいリクエストの受付を停止し、設定されたタイムアウト内で処理中のリクエストを完了します。preStopフックとreadinessプローブの失敗と組み合わせることで、ゼロダウンタイムのローリングアップデートを実現します。

Q15. Virtual ThreadsとWebFluxの選択基準は?

Virtual Threadsを選択: 既存の同期コードベース、シンプルなデバッグニーズ、I/Oバウンドワークロード。WebFluxを選択: ストリーミングユースケース、バックプレッシャー要件、既存のリアクティブインフラ、ノンブロッキングドライバーのみ使用する場合。


12. クイズ

Q1. Spring BootでVirtual Threadsを有効にする1行の設定は?

application.ymlにspring.threads.virtual.enabled=trueを追加します。Tomcatスレッドプールがリクエスト処理用のVirtual Threadsに置き換わります。Spring Boot 3.2+とJava 21+が必要です。

Q2. GraalVM Native Imageでデフォルトでリフレクションが失敗する理由は?

Native ImageはAOTコンパイルを行うため、実行時に動的リフレクション用のクラスメタデータが利用できません。ビルド時にRuntimeHintsRegistrarまたはreflect-config.jsonでリフレクション対象を登録する必要があります。

Q3. Spring Security 6でantMatchers()の代わりに使用するものは?

requestMatchers()がantMatchers()を代替しました。この統一されたAPIはServletとReactive両方の環境で動作し、antMatchersが持っていたServlet APIとの密結合を解消しました。

Q4. TestcontainersのwithReuse(true)は何をしますか?

テスト実行間でDockerコンテナを再利用し、毎回新しいコンテナを作成する代わりに、テスト実行速度を劇的に向上させます。HOME/.testcontainers.propertiesにtestcontainers.reuse.enable=trueの設定が必要です。

Q5. Spring AI Function Callingの仕組みは?

LLMがユーザーの質問を分析し、登録された関数から適切なものを選択してパラメータを抽出します。Spring AIが対応するJavaメソッドを実行し、結果をLLMにフィードバックします。LLMは関数の出力を組み込んだ最終応答を生成します。


参考資料

  1. Spring Boot 3.3 リリースノート
  2. Spring Framework 6 ドキュメント
  3. JEP 444: Virtual Threads
  4. GraalVM Native Image ガイド
  5. Spring AI リファレンス
  6. Spring Security 6 マイグレーションガイド
  7. Testcontainers Spring Boot
  8. Micrometer ドキュメント
  9. OpenTelemetry Java SDK
  10. Spring Data JPA リファレンス
  11. HikariCP 設定
  12. Spring Boot Docker ガイド
  13. Kubernetes Spring Boot ベストプラクティス
  14. OpenRewrite Spring Boot 3 マイグレーション
  15. Baeldung Spring Boot 3 チュートリアル
  16. Spring Boot Actuator ガイド