- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. Spring Boot 3.x ビッグピクチャー
- 2. Java 21 Virtual Threads 実践活用
- 3. GraalVM Native Image
- 4. Spring AI — LLM統合
- 5. Spring Security 6
- 6. データアクセス: JPA、R2DBC、QueryDSL
- 7. Testcontainers統合テスト
- 8. Observability: Micrometer + OpenTelemetry
- 9. DockerとKubernetesデプロイ
- 10. プロダクションチェックリスト
- 11. インタビュー質問 15選
- 12. クイズ
- 参考資料
はじめに
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) |
|---|---|---|
| Servlet | javax.servlet | jakarta.servlet |
| JPA | javax.persistence | jakarta.persistence |
| Validation | javax.validation | jakarta.validation |
| javax.mail | jakarta.mail | |
| Inject | javax.inject | jakarta.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-500MB | 30-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 /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 /app/dependencies/ ./
COPY /app/spring-boot-loader/ ./
COPY /app/snapshot-dependencies/ ./
COPY /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は関数の出力を組み込んだ最終応答を生成します。
参考資料
- Spring Boot 3.3 リリースノート
- Spring Framework 6 ドキュメント
- JEP 444: Virtual Threads
- GraalVM Native Image ガイド
- Spring AI リファレンス
- Spring Security 6 マイグレーションガイド
- Testcontainers Spring Boot
- Micrometer ドキュメント
- OpenTelemetry Java SDK
- Spring Data JPA リファレンス
- HikariCP 設定
- Spring Boot Docker ガイド
- Kubernetes Spring Boot ベストプラクティス
- OpenRewrite Spring Boot 3 マイグレーション
- Baeldung Spring Boot 3 チュートリアル
- Spring Boot Actuator ガイド