SpringBoot with Java

Spring Boot with Java 包括的技術ガイド

目次

  1. はじめに
  2. アーキテクチャの全体像
  3. Spring Boot の概要とアーキテクチャ
  4. 自動構成(Auto-Configuration)の仕組み
  5. Spring Boot Starter
  6. 設定管理とプロファイル
  7. Web 開発(Spring MVC / WebFlux)
  8. データアクセス(Spring Data JPA / JDBC)
  9. セキュリティ(Spring Security)
  10. テスト戦略
  11. Actuator とモニタリング
  12. メッセージングとイベント駆動
  13. キャッシュと性能最適化
  14. マイクロサービスアーキテクチャと Spring Cloud
  15. コンテナ化とデプロイメント
  16. GraalVM Native Image との統合
  17. ベストプラクティスとアンチパターン
  18. まとめ

1. はじめに

1.1 Spring Boot とは

Spring Boot は、Spring Framework をベースとしたアプリケーション開発フレームワークであり、本番環境に対応したスタンドアロンの Spring ベースアプリケーションを迅速に構築するために設計されている。2014年に Pivotal(現 VMware Tanzu / Broadcom)によって初めてリリースされ、以降 Java エコシステムにおけるデファクトスタンダードのフレームワークとして広く普及している。

Spring Boot の核心的な設計思想は「Convention over Configuration(設定より規約)」であり、開発者が最小限の設定で生産性の高い開発を行えるようにすることを目指している。従来の Spring Framework では XML ベースの大量の設定ファイルが必要であったが、Spring Boot はこれを自動設定(Auto-Configuration)メカニズムによって劇的に簡素化した。

1.2 Spring Boot のバージョン体系

バージョンリリース年Java 最低要件主な特徴
Spring Boot 2.7.x2022Java 8Spring Framework 5.3.x ベース、LTS
Spring Boot 3.0.x2022Java 17Jakarta EE 9+、Spring Framework 6.0
Spring Boot 3.1.x2023Java 17Docker Compose サポート、テスト用コンテナ
Spring Boot 3.2.x2023Java 17Virtual Threads、JdbcClient
Spring Boot 3.3.x2024Java 17CDS サポート強化、Base64 プロパティ
Spring Boot 3.4.x2024Java 17構造化ログ、改善されたコンテナイメージ

Spring Boot 3.x 系列における最大の変更点は、javax.* パッケージから jakarta.* パッケージへの全面的な移行である。

1.3 Spring Boot がもたらす価値

  1. 迅速なプロジェクト立ち上げ: Spring Initializr を通じて数秒でプロジェクトの雛形を生成できる
  2. 自動設定: クラスパス上のライブラリを検出し、適切なデフォルト設定を自動的に適用する
  3. 組み込みサーバー: Tomcat、Jetty、Undertow などの組み込みサーバーを内蔵しており、WAR デプロイが不要
  4. 本番環境対応機能: Actuator によるヘルスチェック、メトリクス、監視機能を標準で提供
  5. 豊富なスターター: spring-boot-starter-* による依存関係管理の大幅な簡素化
  6. 外部設定の柔軟性: プロファイルベースの設定管理により環境ごとの設定切り替えが容易
  7. テスト支援: @SpringBootTest をはじめとする包括的なテスト基盤を提供
  8. クラウドネイティブ対応: GraalVM ネイティブイメージ、コンテナ最適化を標準サポート

1.4 Spring Framework との関係

Spring Boot は Spring Framework を「置き換える」ものではなく、Spring Framework の上に構築された「追加レイヤー」である。

Spring Boot = Spring Framework + 自動設定 + 組み込みサーバー + 本番対応機能 + スターター

2. アーキテクチャの全体像

2.1 レイヤードアーキテクチャ

┌──────────────────────────────────────────────────────────────┐
│                     プレゼンテーション層                        │
│         Controller / RestController / View Template           │
├──────────────────────────────────────────────────────────────┤
│                       サービス層                               │
│              ビジネスロジック / トランザクション管理               │
├──────────────────────────────────────────────────────────────┤
│                     データアクセス層                            │
│            Repository / JPA Entity / DAO                      │
├──────────────────────────────────────────────────────────────┤
│                       インフラ層                               │
│       DataSource / Cache / Messaging / External APIs          │
└──────────────────────────────────────────────────────────────┘

2.2 起動プロセスの詳細フロー

1. main() メソッド実行
   └── SpringApplication.run() 呼び出し
       ├── 2. SpringApplication インスタンス生成
       │   ├── WebApplicationType の推定 (SERVLET / REACTIVE / NONE)
       │   ├── ApplicationContextInitializer の検出
       │   └── ApplicationListener の検出
       ├── 3. SpringApplicationRunListeners の通知
       ├── 4. 環境(Environment)の準備
       │   ├── application.properties / .yml の読み込み
       │   ├── 環境変数の読み込み
       │   └── プロファイルのアクティベーション
       ├── 5. ApplicationContext の生成
       ├── 6. ApplicationContext の準備
       ├── 7. ApplicationContext のリフレッシュ
       │   ├── Auto-Configuration の適用
       │   ├── Bean のインスタンス化と依存性注入
       │   └── 組み込みサーバーの起動
       └── 8. Runner の実行
           ├── ApplicationRunner / CommandLineRunner の実行
           └── ApplicationReadyEvent 発行

3. Spring Boot の概要とアーキテクチャ

3.1 DI(Dependency Injection)/ IoC(Inversion of Control)

Spring Framework の中核は DI/IoC コンテナである。IoC とは「制御の反転」を意味し、オブジェクトの生成やライフサイクル管理をフレームワーク側が担うデザインパターンである。

@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    // コンストラクタインジェクション(推奨)
    public OrderServiceImpl(OrderRepository orderRepository,
                            PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    @Override
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);
        paymentService.processPayment(order);
        return order;
    }
}

DI の方式比較

方式推奨度説明
コンストラクタインジェクション最も推奨不変性保証、テスト容易性
セッターインジェクション条件付きオプショナルな依存に使用
フィールドインジェクション非推奨テスト困難、不変性なし

3.2 Bean のスコープ

スコープ説明用途
singletonアプリケーション全体で1つのインスタンス(デフォルト)ステートレスなサービス
prototype取得ごとに新しいインスタンスステートフルなオブジェクト
requestHTTPリクエストごとに1つWebアプリケーション
sessionHTTPセッションごとに1つユーザーセッション管理

3.3 AOP(Aspect-Oriented Programming)

@Aspect
@Component
public class PerformanceMonitorAspect {
    private static final Logger log = LoggerFactory.getLogger(PerformanceMonitorAspect.class);

    @Around("@annotation(Monitored)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        long startTime = System.nanoTime();
        try {
            Object result = joinPoint.proceed();
            long duration = (System.nanoTime() - startTime) / 1_000_000;
            log.info("Method {} executed in {} ms", methodName, duration);
            return result;
        } catch (Exception e) {
            long duration = (System.nanoTime() - startTime) / 1_000_000;
            log.error("Method {} failed after {} ms: {}", methodName, duration, e.getMessage());
            throw e;
        }
    }
}

3.4 Spring のイベントモデル

// イベント発行
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public Order createOrder(OrderRequest request) {
        Order order = // ... 注文作成ロジック
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
        return order;
    }
}

// イベントリスナー
@Component
public class OrderEventListener {
    @EventListener
    @Async
    public void handleOrderCreated(OrderCreatedEvent event) {
        sendConfirmationEmail(event.getOrder());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderAfterCommit(OrderCreatedEvent event) {
        updateAnalytics(event.getOrder());
    }
}

4. 自動構成(Auto-Configuration)の仕組み

4.1 Auto-Configuration の原理

Spring Boot の自動構成は、クラスパス上のライブラリを検出し、適切な Bean を自動的に設定する仕組みである。

  1. @EnableAutoConfigurationMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports を読み込む
  2. 各 Auto-Configuration クラスの条件アノテーションを評価
  3. 条件を満たす設定クラスが有効化され、Bean が登録される

4.2 条件アノテーション(@Conditional)

アノテーション条件
@ConditionalOnClass指定クラスがクラスパスに存在する場合
@ConditionalOnMissingClass指定クラスがクラスパスに存在しない場合
@ConditionalOnBean指定Beanが存在する場合
@ConditionalOnMissingBean指定Beanが存在しない場合
@ConditionalOnProperty指定プロパティが特定の値の場合
@ConditionalOnResource指定リソースが存在する場合
@ConditionalOnWebApplicationWebアプリケーションの場合

4.3 Auto-Configuration のカスタム実装例

@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(prefix = "app.datasource.monitoring", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(DataSourceMonitoringProperties.class)
public class DataSourceMonitoringAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public DataSourceMonitor dataSourceMonitor(
            DataSource dataSource,
            DataSourceMonitoringProperties properties) {
        return new DataSourceMonitor(dataSource, properties.getInterval());
    }
}

@ConfigurationProperties(prefix = "app.datasource.monitoring")
public class DataSourceMonitoringProperties {
    private boolean enabled = false;
    private Duration interval = Duration.ofSeconds(30);
    // getter / setter
}

4.4 自動構成の確認とデバッグ

debug: true  # 起動時にAutoConfigurationレポートを表示
management:
  endpoints:
    web:
      exposure:
        include: conditions

5. Spring Boot Starter

5.1 pom.xml の設定例(Maven)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.0</version>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

5.2 build.gradle の設定例(Gradle)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

6. 設定管理とプロファイル

6.1 application.yml の包括的な設定例

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:postgresql://localhost:5432/orderdb
    username: ${DB_USERNAME:order_user}
    password: ${DB_PASSWORD:}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      max-lifetime: 1800000
      connection-timeout: 30000
      leak-detection-threshold: 60000
  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false
    properties:
      hibernate:
        format_sql: true
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
        default_batch_fetch_size: 16
  flyway:
    enabled: true
    locations: classpath:db/migration
  jackson:
    default-property-inclusion: non_null
    serialization:
      write-dates-as-timestamps: false
    deserialization:
      fail-on-unknown-properties: false
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      lettuce:
        pool:
          max-active: 16
          max-idle: 8
  kafka:
    bootstrap-servers: ${KAFKA_BROKERS:localhost:9092}
    consumer:
      group-id: order-service-group
      auto-offset-reset: earliest
      enable-auto-commit: false
    producer:
      retries: 3
      acks: all

server:
  port: 8080
  shutdown: graceful
  tomcat:
    max-threads: 200
    min-spare-threads: 10
  compression:
    enabled: true
    mime-types: application/json,application/xml
    min-response-size: 1024

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,env,loggers
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        http.server.requests: true

logging:
  level:
    root: INFO
    com.example.myapp: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n"

6.2 プロファイル(Profile)

application.yml                     <- ベース設定(常に読み込み)
    ↓ マージ
application-{profile}.yml           <- プロファイル固有設定(上書き)
    ↓ マージ
環境変数                             <- さらに上書き
    ↓ マージ
コマンドライン引数                    <- 最高優先度
# application-dev.yml(開発環境)
spring:
  datasource:
    url: jdbc:h2:mem:devdb
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

# application-prod.yml(本番環境)
spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST}:5432/orderdb
    hikari:
      maximum-pool-size: 30
  jpa:
    show-sql: false
server:
  port: 8443
  ssl:
    enabled: true

6.3 外部設定のバインディング

@ConfigurationProperties(prefix = "app.order")
@Validated
public class OrderProperties {
    @NotNull
    private Integer maxItemsPerOrder = 100;
    @NotBlank
    private String defaultCurrency = "JPY";
    private Duration expiryDuration = Duration.ofMinutes(30);
    private Retry retry = new Retry();

    public static class Retry {
        private int maxAttempts = 3;
        private Duration delay = Duration.ofSeconds(1);
        private double multiplier = 2.0;
    }
}

6.4 設定の優先順位(高い順)

  1. コマンドライン引数 (--server.port=9090)
  2. Java システムプロパティ (-Dserver.port=9090)
  3. OS 環境変数 (SERVER_PORT=9090)
  4. プロファイル固有の application-{profile}.yml
  5. パッケージ外の application.yml
  6. パッケージ内の application.yml
  7. @PropertySource で指定されたファイル
  8. デフォルト値

7. Web 開発(Spring MVC / WebFlux)

7.1 REST Controller の実装例

@RestController
@RequestMapping("/api/v1/orders")
@Validated
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public ResponseEntity<Page<OrderResponse>> getOrders(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) OrderStatus status) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return ResponseEntity.ok(orderService.getOrders(status, pageable));
    }

    @GetMapping("/{orderId}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable @Positive Long orderId) {
        return ResponseEntity.ok(orderService.getOrderById(orderId));
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @RequestBody @Valid CreateOrderRequest request,
            @AuthenticationPrincipal UserDetails userDetails) {
        OrderResponse order = orderService.createOrder(request, userDetails.getUsername());
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest().path("/{id}")
                .buildAndExpand(order.getId()).toUri();
        return ResponseEntity.created(location).body(order);
    }

    @DeleteMapping("/{orderId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void cancelOrder(@PathVariable @Positive Long orderId) {
        orderService.cancelOrder(orderId);
    }
}

7.2 DTO(Data Transfer Object)

public record CreateOrderRequest(
    @NotBlank String customerId,
    @NotEmpty @Size(max = 100) List<@Valid OrderItemRequest> items,
    @NotNull String shippingAddress,
    String notes
) {}

public record OrderResponse(
    Long id, String customerId, OrderStatus status,
    List<OrderItemResponse> items, BigDecimal totalAmount,
    String shippingAddress, LocalDateTime createdAt, LocalDateTime updatedAt
) {}

7.3 グローバル例外ハンドラー

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse(404, "NOT_FOUND", ex.getMessage(), LocalDateTime.now()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
            .map(fe -> new FieldError(fe.getField(), fe.getDefaultMessage())).toList();
        return ResponseEntity.badRequest()
            .body(new ErrorResponse(400, "VALIDATION_ERROR", "入力値が不正です", LocalDateTime.now(), fieldErrors));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        log.error("Unexpected error occurred", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse(500, "INTERNAL_ERROR", "内部エラーが発生しました", LocalDateTime.now()));
    }
}

7.4 Spring WebFlux(リアクティブ)

@RestController
@RequestMapping("/api/v1/reactive/orders")
public class ReactiveOrderController {
    private final ReactiveOrderService orderService;

    @GetMapping("/{orderId}")
    public Mono<OrderResponse> getOrder(@PathVariable Long orderId) {
        return orderService.getOrderById(orderId)
            .switchIfEmpty(Mono.error(new ResourceNotFoundException("Order not found")));
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<OrderEvent> streamOrderEvents() {
        return orderService.getOrderEventStream();
    }
}

7.5 Spring MVC と WebFlux の比較

特性Spring MVCSpring WebFlux
プログラミングモデル同期・ブロッキング非同期・ノンブロッキング
基盤Servlet APIReactive Streams
サーバーTomcat, Jetty, UndertowNetty, Tomcat, Jetty
スレッドモデルThread-per-requestEvent Loop
戻り値通常のオブジェクトMono<T> / Flux<T>
ユースケース一般的なCRUDアプリ高並行性、ストリーミング
DB アクセスJDBC / JPAR2DBC

7.6 フィルターとインターセプター

@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);
        response.setHeader("X-Request-Id", requestId);
        long startTime = System.currentTimeMillis();
        try {
            filterChain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            log.info("Request: {} {} - Status: {} - Duration: {}ms",
                request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
            MDC.clear();
        }
    }
}

8. データアクセス(Spring Data JPA / JDBC)

8.1 エンティティ定義

@Entity
@Table(name = "orders", indexes = {
    @Index(name = "idx_orders_customer_id", columnList = "customerId"),
    @Index(name = "idx_orders_status", columnList = "status")
})
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String customerId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private OrderStatus status = OrderStatus.PENDING;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;

    @Version
    private Long version;  // 楽観的ロック
}

8.2 リポジトリ定義

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerIdOrderByCreatedAtDesc(String customerId);
    Page<Order> findByStatus(OrderStatus status, Pageable pageable);
    long countByCustomerIdAndStatus(String customerId, OrderStatus status);

    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    @Query(value = """
        SELECT o.customer_id, COUNT(*) as order_count, SUM(o.total_amount) as total_spent
        FROM orders o WHERE o.created_at >= :since
        GROUP BY o.customer_id HAVING COUNT(*) >= :minOrders
        ORDER BY total_spent DESC
        """, nativeQuery = true)
    List<Object[]> findTopCustomers(@Param("since") LocalDateTime since, @Param("minOrders") int minOrders);

    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
}

8.3 Specification による動的クエリ

public class OrderSpecifications {
    public static Specification<Order> hasStatus(OrderStatus status) {
        return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status);
    }
    public static Specification<Order> hasCustomerId(String customerId) {
        return (root, query, cb) -> customerId == null ? null : cb.equal(root.get("customerId"), customerId);
    }
    public static Specification<Order> createdBetween(LocalDateTime start, LocalDateTime end) {
        return (root, query, cb) -> {
            if (start == null && end == null) return null;
            if (start != null && end != null) return cb.between(root.get("createdAt"), start, end);
            if (start != null) return cb.greaterThanOrEqualTo(root.get("createdAt"), start);
            return cb.lessThanOrEqualTo(root.get("createdAt"), end);
        };
    }
}

8.4 トランザクション管理

@Service
@Transactional(readOnly = true)
public class OrderService {
    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        inventoryService.reserveItems(order.getItems());
        orderRepository.save(order);
        return OrderResponse.from(order);
    }

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,
                   timeout = 30, rollbackFor = {BusinessException.class})
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
        paymentService.processPayment(order);
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);
    }
}

8.5 データベースマイグレーション(Flyway)

-- V1__create_orders_table.sql
CREATE TABLE orders (
    id          BIGSERIAL PRIMARY KEY,
    customer_id VARCHAR(50) NOT NULL,
    status      VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0,
    created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    version     BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);

9. セキュリティ(Spring Security)

9.1 Spring Security アーキテクチャ

HTTP Request → Security Filter Chain → DispatcherServlet → Controller
    ├── CorsFilter
    ├── CsrfFilter
    ├── BasicAuthenticationFilter
    ├── BearerTokenAuthFilter
    ├── AuthorizationFilter
    └── ExceptionTranslationFilter

9.2 セキュリティ設定(Spring Boot 3.x / Spring Security 6.x)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.DELETE, "/api/v1/orders/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                UsernamePasswordAuthenticationFilter.class)
            .headers(headers -> headers
                .frameOptions(frame -> frame.deny())
                .httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000).includeSubDomains(true)))
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

9.3 JWT 認証

@Component
public class JwtTokenProvider {
    @Value("${app.jwt.secret}")
    private String jwtSecret;

    public String generateAccessToken(UserDetails userDetails) {
        return Jwts.builder()
            .claims(Map.of("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList()))
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(getSigningKey())
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

9.4 メソッドレベルセキュリティ

@PreAuthorize("hasRole('ADMIN') or #customerId == authentication.name")
public List<Order> getOrdersByCustomer(String customerId) { ... }

@PostAuthorize("returnObject.customerId == authentication.name or hasRole('ADMIN')")
public Order getOrder(Long orderId) { ... }

9.5 OAuth 2.0 Resource Server 設定

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/my-realm

10. テスト戦略

10.1 単体テスト

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock private OrderRepository orderRepository;
    @Mock private PaymentService paymentService;
    @InjectMocks private OrderService orderService;

    @Test
    @DisplayName("正常な注文作成が成功する")
    void createOrder_withValidRequest_shouldSucceed() {
        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
        OrderResponse response = orderService.createOrder(request, "customer-1");
        assertThat(response).isNotNull();
        assertThat(response.status()).isEqualTo(OrderStatus.PENDING);
        verify(orderRepository).save(any(Order.class));
    }
}

10.2 統合テスト

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class OrderControllerIntegrationTest {
    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper objectMapper;

    @Test
    @WithMockUser(roles = "USER")
    void createOrder_shouldReturn201() throws Exception {
        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.status").value("PENDING"));
    }
}

10.3 スライステスト

@WebMvcTest(OrderController.class)    // Controller 層のみ
@DataJpaTest                          // Repository 層のみ

10.4 Testcontainers

@SpringBootTest
@Testcontainers
class OrderServiceWithDatabaseTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

11. Actuator とモニタリング

11.1 主要エンドポイント

エンドポイント説明
/actuator/healthヘルスチェック
/actuator/infoアプリケーション情報
/actuator/metricsメトリクス一覧
/actuator/prometheusPrometheus 形式メトリクス
/actuator/env環境プロパティ
/actuator/loggersログレベル管理
/actuator/threaddumpスレッドダンプ

11.2 カスタムメトリクス(Micrometer)

@Component
public class OrderMetrics {
    private final Counter orderCreatedCounter;
    private final Timer orderProcessingTimer;

    public OrderMetrics(MeterRegistry registry) {
        this.orderCreatedCounter = Counter.builder("orders.created")
            .description("Number of orders created").register(registry);
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .publishPercentiles(0.5, 0.95, 0.99)
            .publishPercentileHistogram().register(registry);
    }
}

11.3 Kubernetes プローブ設定

management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        readiness:
          include: db, redis, kafka
        liveness:
          include: livenessState

12. メッセージングとイベント駆動

12.1 Apache Kafka との統合

// Producer
@Service
public class OrderEventProducer {
    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public CompletableFuture<SendResult<String, OrderEvent>> publishOrderCreated(Order order) {
        OrderEvent event = new OrderEvent(UUID.randomUUID().toString(), "ORDER_CREATED",
            order.getId(), order.getCustomerId(), order.getTotalAmount(), Instant.now());
        return kafkaTemplate.send("order-events", order.getId().toString(), event);
    }
}

// Consumer
@Component
public class OrderEventConsumer {
    @KafkaListener(topics = "order-events", groupId = "analytics-service-group")
    @RetryableTopic(attempts = "3", backoff = @Backoff(delay = 1000, multiplier = 2.0))
    public void handleOrderEvent(@Payload OrderEvent event, Acknowledgment ack) {
        analyticsService.processOrderEvent(event);
        ack.acknowledge();
    }

    @DltHandler
    public void handleDlt(OrderEvent event) {
        log.error("Message exhausted retries: {}", event.eventId());
    }
}

12.2 RabbitMQ との統合

@Configuration
public class RabbitMQConfig {
    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable("order.queue")
            .withArgument("x-dead-letter-exchange", "dlx.exchange").build();
    }

    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange("order.exchange");
    }
}

13. キャッシュと性能最適化

13.1 Spring Cache Abstraction

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30)).disableCachingNullValues();
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(Map.of(
                "orders", defaultConfig.entryTtl(Duration.ofMinutes(10)),
                "products", defaultConfig.entryTtl(Duration.ofHours(1))))
            .build();
    }
}

@Service
public class ProductService {
    @Cacheable(value = "products", key = "#productId", unless = "#result == null")
    public Product getProduct(String productId) { ... }

    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) { ... }

    @CacheEvict(value = "products", key = "#productId")
    public void deleteProduct(String productId) { ... }
}

13.2 非同期処理

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

13.3 HTTP クライアント(Spring Boot 3.2+)

@HttpExchange("/api/v1")
public interface ExternalOrderClient {
    @GetExchange("/orders/{id}")
    OrderResponse getOrder(@PathVariable String id);

    @PostExchange("/orders")
    OrderResponse createOrder(@RequestBody CreateOrderRequest request);
}

14. マイクロサービスアーキテクチャと Spring Cloud

14.1 Spring Cloud エコシステム

コンポーネント役割
Service Discovery (Eureka/Consul)サービス登録・発見
API Gateway (Spring Cloud Gateway)ルーティング、レート制限
Circuit Breaker (Resilience4j)障害分離
Config Server設定の一元管理
Distributed Tracing (Micrometer/OTEL)分散トレーシング
Load Balancer (Spring Cloud LB)クライアントサイド負荷分散
Stream (Kafka/RabbitMQ)イベントストリーミング
OpenFeign宣言的 HTTP クライアント

14.2 サーキットブレーカー(Resilience4j)

@Service
public class PaymentServiceClient {
    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    @Retry(name = "paymentService")
    @Bulkhead(name = "paymentService")
    public PaymentResult processPayment(PaymentRequest request) {
        return restClient.post().uri("/api/v1/payments").body(request)
            .retrieve().body(PaymentResult.class);
    }

    private PaymentResult paymentFallback(PaymentRequest request, Throwable t) {
        return new PaymentResult("PENDING", "Payment queued for retry");
    }
}
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 3
  retry:
    instances:
      paymentService:
        max-attempts: 3
        wait-duration: 1s
        exponential-backoff-multiplier: 2

14.3 分散トレーシング

management:
  tracing:
    sampling:
      probability: 1.0
logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

15. コンテナ化とデプロイメント

15.1 Dockerfile(マルチステージビルド)

FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -B
COPY src ./src
RUN ./mvnw clean package -DskipTests -B

FROM eclipse-temurin:21-jdk-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
WORKDIR /app
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
EXPOSE 8080

ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher"]

15.2 Kubernetes マニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: order-service
          image: myregistry/order-service:1.0.0
          resources:
            requests: { cpu: 500m, memory: 512Mi }
            limits: { cpu: "1", memory: 1Gi }
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: 8080 }
            initialDelaySeconds: 30
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            initialDelaySeconds: 15
          lifecycle:
            preStop:
              exec: { command: ["sh", "-c", "sleep 10"] }
      terminationGracePeriodSeconds: 60
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

16. GraalVM Native Image との統合

16.1 メリットとデメリット

特性JVM モードNative Image
起動時間数秒〜十数秒ミリ秒〜数百ミリ秒
メモリ使用量高い(数百MB)低い(数十MB)
ピークスループット高い(JIT最適化)やや低い
ビルド時間短い長い(数分〜十数分)
リフレクション完全サポート制限あり(要ヒント)
ユースケース長時間稼働サービスサーバーレス、CLI

16.2 Native Image のビルド

./mvnw -Pnative native:compile
./mvnw -Pnative spring-boot:build-image

16.3 AOT ヒントの提供

@RegisterReflectionForBinding({OrderRequest.class, OrderResponse.class})
@Configuration
public class NativeHintsConfig implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.resources().registerPattern("db/migration/*");
        hints.reflection().registerType(Order.class,
            MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
    }
}

17. ベストプラクティスとアンチパターン

17.1 ベストプラクティス

プロジェクト構成

  1. パッケージ構成をドメインベースまたはレイヤーベースで統一する
  2. エンティティをAPIレスポンスとして直接返さず、DTOを使用する
  3. Bean Validation を活用し、Controller 層で入力検証する
  4. @RestControllerAdvice で例外処理をグローバルに統一する

コーディング 5. フィールドインジェクションではなくコンストラクタインジェクションを使用する 6. 可能な限りイミュータブルなオブジェクト(record 等)を使用する 7. SLF4J + Logback を使用し、構造化ログを出力する 8. ハードコードせず、@ConfigurationProperties で設定を外部化する

データアクセス 9. JOIN FETCH@EntityGraph で N+1 問題を回避する 10. spring.jpa.open-in-view=false を設定する 11. Flyway / Liquibase でスキーマ変更を管理する

運用 12. Actuator を活用してヘルスチェック、メトリクスを提供する 13. server.shutdown=graceful を設定する 14. パスワードを設定ファイルに直書きしない

17.2 アンチパターン

アンチパターン問題推奨
エンティティをAPIレスポンスに直接使用内部構造の露出DTO を使用
@Autowired フィールドインジェクションテスト困難コンストラクタインジェクション
spring.jpa.open-in-view=trueDB接続長期保持false に設定
ddl-auto=update を本番で使用予期しないスキーマ変更validate + Flyway
大きな @SpringBootTest ばかりテスト実行が遅いスライステスト活用
Actuator エンドポイントの全公開セキュリティリスク必要なもののみ公開

17.3 パフォーマンスチェックリスト

  • HikariCP の接続プールサイズは適切か
  • JPA の N+1 問題は解消されているか
  • open-in-view は無効になっているか
  • 適切なインデックスが設定されているか
  • キャッシュは適切に活用されているか
  • HTTP レスポンスの圧縮は有効か
  • JVM のヒープサイズは適切か
  • GC アルゴリズムは適切か(G1GC / ZGC)

18. まとめ

18.1 Spring Boot の全体像

カテゴリ主要技術
基盤DI/IoC、AOP、Bean管理第2〜3章
自動構成Auto-Configuration、Starter第4〜5章
設定管理Properties、Profiles、外部設定第6章
Web 開発Spring MVC、WebFlux第7章
データアクセスJPA、JDBC、トランザクション第8章
セキュリティSpring Security、JWT、OAuth2第9章
テストJUnit、MockMvc、Testcontainers第10章
運用監視Actuator、Micrometer第11章
メッセージングKafka、RabbitMQ第12章
性能最適化キャッシュ、非同期、接続プール第13章
マイクロサービスSpring Cloud、サーキットブレーカー第14章
コンテナ化Docker、Kubernetes第15章
ネイティブ化GraalVM Native Image第16章

18.2 Spring Boot の進化の方向性

  1. Virtual Threads(Project Loom): Java 21 の Virtual Threads により、同期コードで非同期並みのスケーラビリティを実現
  2. GraalVM Native Image: AOT コンパイルによる起動時間の短縮とメモリ削減
  3. Observability: OpenTelemetry との統合強化
  4. Spring AI: AI/LLM との統合フレームワーク
  5. CRaC(Coordinated Restore at Checkpoint): チェックポイント/リストアによる起動高速化

18.3 学習リソース

リソースURL
Spring Boot 公式ドキュメントhttps://docs.spring.io/spring-boot/docs/current/reference/html/
Spring Initializrhttps://start.spring.io/
Spring Guideshttps://spring.io/guides
Baeldung (チュートリアル)https://www.baeldung.com/
Spring Boot GitHubhttps://github.com/spring-projects/spring-boot

本書は Spring Boot 3.x / Java 21 をベースとして執筆されている。Spring Boot のバージョンにより、一部の機能やAPIが異なる場合がある。最新の情報は公式ドキュメントを参照のこと。