Spring は Java エコシステムで最も人気のあるアプリケーションフレームワークであり、IoC コンテナ、AOP、MVC、トランザクション管理などの機能を提供し、エンタープライズアプリケーション開発を大幅に簡略化します。
Spring コアモジュール
Spring フレームワークは複数のモジュールで構成されており、必要に応じて導入できます:
| モジュール | 説明 |
|---|---|
| spring-core | IoC コンテナのコア基盤 |
| spring-beans | BeanFactory — Bean の生成と管理を担当 |
| spring-context | ApplicationContext — beans を基盤に、より豊富な機能を提供 |
| spring-aop | アスペクト指向プログラミングのサポート |
| spring-webmvc | Spring MVC フレームワーク |
| spring-tx | トランザクション管理 |
| spring-data-jpa | JPA 統合 |
| spring-boot | 設定より規約、自動設定 |
IoC(制御の反転)と DI(依存性注入)
IoC とは
従来のモデルでは、オブジェクトは自身の依存オブジェクトを自ら生成します:
public class UserService {
// 自分で new — 結合度が高い
private UserRepository repo = new UserRepositoryImpl();
}
IoC(Inversion of Control)は、オブジェクトの生成と依存関係の管理を Spring コンテナに委ねます。オブジェクトは必要なものを宣言するだけで、コンテナがそれを注入します:
@Service
public class UserService {
private final UserRepository repo;
// Spring が自動的に注入する
public UserService(UserRepository repo) {
this.repo = repo;
}
}
制御がオブジェクト自身からコンテナへ「反転」する — これが IoC の核心思想です。
DI の 3 つの方法
1. コンストラクタ注入(推奨)
@Service
public class OrderService {
private final UserRepository userRepo;
private final ProductRepository productRepo;
// コンストラクタが 1 つの場合、@Autowired は省略可
public OrderService(UserRepository userRepo, ProductRepository productRepo) {
this.userRepo = userRepo;
this.productRepo = productRepo;
}
}
利点:依存を final として宣言でき、不変性が保たれ、テストしやすく、オブジェクト構築時点で依存が確実に揃います。
2. セッター注入
@Service
public class NotificationService {
private EmailSender emailSender;
@Autowired
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
省略可能な依存や、実行時に実装を動的に切り替える必要があるシナリオに適しています。
3. フィールド注入(非推奨)
@Service
public class UserService {
@Autowired
private UserRepository userRepo; // final 宣言不可、単体テストが困難
}
コードは簡潔ですが final が使えず、コンテナ外でインスタンス化すると依存が null になるため、本番コードでの使用は推奨しません。
Bean 管理
Bean の定義方法
アノテーション方式(最も一般的)
@Component // 汎用コンポーネント
@Service // ビジネス層
@Repository // データアクセス層(JPA 例外変換も有効化)
@Controller // MVC コントローラ
@RestController // = @Controller + @ResponseBody
Java Config 方式
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public EmailSender emailSender() {
return new SmtpEmailSender();
}
}
XML 方式(レガシープロジェクトで引き続き使用)
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="userRepository"/>
</bean>
Bean のスコープ
@Component
@Scope("singleton") // デフォルト — コンテナ内にインスタンスは 1 つだけ
public class AppConfig { }
@Component
@Scope("prototype") // getBean() のたびに新しいインスタンスを生成
public class RequestProcessor { }
// Web 環境専用
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class RequestContext { }
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class UserSession { }
| スコープ | 説明 |
|---|---|
| singleton | デフォルト — コンテナ内で唯一のインスタンス |
| prototype | 注入 / getBean() のたびに新しいインスタンスを生成 |
| request | HTTP リクエストごとに 1 インスタンス(Web 環境) |
| session | HTTP セッションごとに 1 インスタンス(Web 環境) |
Bean のライフサイクル
インスタンス化
→ プロパティ注入
→ BeanNameAware / BeanFactoryAware / ApplicationContextAware
→ BeanPostProcessor#postProcessBeforeInitialization
→ @PostConstruct
→ InitializingBean#afterPropertiesSet
→ init-method
→ BeanPostProcessor#postProcessAfterInitialization
→ 【Bean 使用可能】
→ @PreDestroy
→ DisposableBean#destroy
→ destroy-method
→ 破棄
よく使われるライフサイクルフック:
@Component
public class DataSourcePool {
@PostConstruct
public void init() {
// Bean の初期化完了後に実行 — コネクションプールのウォームアップに最適
System.out.println("コネクションプール初期化完了");
}
@PreDestroy
public void destroy() {
// コンテナのシャットダウン前に実行 — リソース解放に最適
System.out.println("コネクションプールリソースを解放");
}
}
よく使われるアノテーション
注入と設定
// 型による自動注入
@Autowired
private UserRepository userRepo;
// 同じ型の Bean が複数ある場合、名前で指定
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;
// @Autowired + @Qualifier と同等、よりシンプル(JSR-250 標準)
@Resource(name = "mysqlUserRepo")
private UserRepository userRepo;
// 設定ファイルの値を注入
@Value("${app.name}")
private String appName;
// デフォルト値付きで注入
@Value("${app.timeout:30}")
private int timeout;
// SpEL 式を注入
@Value("#{systemProperties['user.home']}")
private String userHome;
設定クラスのアノテーション
@Configuration // 設定クラスを示す
@ComponentScan("com.example") // スキャン対象のパッケージを指定
@PropertySource("classpath:app.properties") // 追加のプロパティファイルを読み込む
@Import(SecurityConfig.class) // 別の設定クラスをインポート
public class AppConfig { }
条件付きアセンブリ(Spring Boot でよく使用)
// プロパティが存在し、指定された値の場合のみアセンブル
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
@Bean
public CacheManager cacheManager() {
return new RedisCacheManager(...);
}
// 指定した型の Bean が存在しない場合のみアセンブル(デフォルト実装を提供)
@ConditionalOnMissingBean(DataSource.class)
@Bean
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder().build();
}
// 指定したクラスがクラスパス上にある場合のみアセンブル
@ConditionalOnClass(RedisTemplate.class)
@Configuration
public class RedisAutoConfig { }
AOP(アスペクト指向プログラミング)
AOP を使うと、ビジネスコードを変更せずに、ロギング、認可チェック、パフォーマンス監視、トランザクションなどの横断的関心事を一元管理できます。
コアコンセプト
| コンセプト | 説明 |
|---|---|
| Aspect(アスペクト) | 横断的ロジックをカプセル化したクラス。@Aspect でアノテート |
| Pointcut(ポイントカット) | どのメソッドに適用するかを定義する式 |
| Advice(アドバイス) | ポイントカットがトリガーされたときに実行されるコード |
| JoinPoint(ジョインポイント) | インターセプト可能な実行位置(メソッド呼び出し、例外など) |
| Weaving(ウィービング) | アスペクトのロジックをターゲットオブジェクトに適用するプロセス |
アドバイスの種類
@Aspect
@Component
public class LoggingAspect {
// ポイントカット式 — service パッケージ配下の全メソッドにマッチ
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// メソッド実行前
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("メソッド呼び出し:" + joinPoint.getSignature().getName());
}
// メソッド実行後(例外の有無に関わらず)
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("メソッド実行完了");
}
// 正常にリターンした後
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("戻り値:" + result);
}
// 例外がスローされた後
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void logAfterThrowing(Exception ex) {
System.out.println("例外:" + ex.getMessage());
}
// ラウンドアドバイス(最も強力 — メソッドを実行するかどうかを制御できる)
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed(); // ターゲットメソッドを実行
long elapsed = System.currentTimeMillis() - start;
System.out.println(pjp.getSignature() + " 処理時間:" + elapsed + "ms");
return result;
} catch (Exception e) {
System.out.println("例外発生:" + e.getMessage());
throw e;
}
}
}
ポイントカット式
// 特定のメソッドに厳密にマッチ
execution(public String com.example.service.UserService.findById(Long))
// クラス内の全メソッドにマッチ
execution(* com.example.service.UserService.*(..))
// パッケージとそのサブパッケージ内の全メソッドにマッチ(.. は任意のサブパッケージ)
execution(* com.example.service..*.*(..))
// 特定のアノテーションが付いたメソッドにマッチ
@annotation(com.example.annotation.Log)
// パッケージ内の全型にマッチ
within(com.example.service.*)
// 組み合わせ(get で始まるメソッドを除外)
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))")
AOP 実践:統一操作ログ
// カスタムアノテーション
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String value() default "";
}
// アスペクト実装
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private LogService logService;
@Around("@annotation(operationLog)")
public Object record(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String operation = operationLog.value();
try {
Object result = pjp.proceed();
logService.save(username, operation, "SUCCESS");
return result;
} catch (Exception e) {
logService.save(username, operation, "FAILED: " + e.getMessage());
throw e;
}
}
}
// アノテーションの使用
@Service
public class UserService {
@OperationLog("ユーザー削除")
public void deleteUser(Long id) {
// ビジネスコードはログを意識しなくてよい
}
}
Spring MVC
リクエスト処理フロー
クライアントリクエスト
→ DispatcherServlet(フロントコントローラ)
→ HandlerMapping(対応する Controller メソッドを検索)
→ HandlerAdapter(Controller メソッドを実行)
→ ModelAndView または @ResponseBody データを返す
→ ViewResolver(ビュー名の場合、対応するビューを検索)
→ レンダリング → クライアントへレスポンス
Controller のよく使うアノテーション
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// GET /api/users
@GetMapping
public List<User> list() {
return userService.findAll();
}
// GET /api/users/1
@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody @Valid CreateUserRequest req) {
return userService.create(req);
}
// PUT /api/users/1
@PutMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest req) {
return userService.update(id, req);
}
// DELETE /api/users/1
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
// GET /api/users?page=0&size=10&keyword=Alice
@GetMapping("/search")
public Page<User> search(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword
) {
return userService.search(keyword, PageRequest.of(page, size));
}
}
パラメータバインディング
// パスパラメータ
@GetMapping("/{id}")
public User get(@PathVariable Long id) { ... }
// クエリパラメータ(デフォルト値付き)
@GetMapping
public List<User> list(
@RequestParam String name,
@RequestParam(defaultValue = "0") int page
) { ... }
// リクエストボディ(JSON → オブジェクト)
@PostMapping
public User create(@RequestBody CreateUserRequest req) { ... }
// リクエストヘッダ
@GetMapping
public String auth(@RequestHeader("Authorization") String token) { ... }
// Cookie
@GetMapping
public String session(@CookieValue("sessionId") String sid) { ... }
// クエリパラメータをオブジェクトに自動バインド(?name=Alice&age=25 → UserSearchRequest)
@GetMapping
public Page<User> search(UserSearchRequest req) { ... }
グローバル例外ハンドリング
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(EntityNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors()
.stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.toList());
return new ErrorResponse("VALIDATION_FAILED", errors.toString());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
return new ErrorResponse("INTERNAL_ERROR", "サーバー内部エラー");
}
}
Spring Boot
自動設定の仕組み
Spring Boot は @SpringBootApplication によって自動設定を有効にします。これは 3 つのアノテーションの組み合わせです:
@SpringBootConfiguration // @Configuration と同等
@EnableAutoConfiguration // コア:自動設定を有効化
@ComponentScan // 現在のパッケージとサブパッケージをスキャン
public @interface SpringBootApplication { }
@EnableAutoConfiguration は AutoConfigurationImportSelector を通じて META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports に列挙された全自動設定クラスを読み込み、各 @ConditionalOnXxx アノテーションで条件を評価して Bean を選択的に登録します。
フロー全体を簡略化すると:
起動
→ AutoConfiguration.imports をスキャン
→ 各 @ConditionalOnXxx 条件をチェック
→ 条件を満たす → Bean を登録
→ 条件を満たさない → スキップ
設定ファイル
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
server:
port: 8080
servlet:
context-path: /api
app:
jwt:
secret: my-secret-key
expiration: 86400
マルチ環境設定
# application.yml
spring:
profiles:
active: dev # デフォルトで dev 環境を有効化
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-server:3306/mydb
設定をクラスにバインド
@ConfigurationProperties(prefix = "app.jwt")
@Component
public class JwtProperties {
private String secret;
private long expiration;
// getters & setters
}
トランザクション管理
基本的な使い方
@Service
public class TransferService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepo.findById(fromId).orElseThrow();
Account to = accountRepo.findById(toId).orElseThrow();
from.deduct(amount);
to.add(amount);
accountRepo.save(from);
accountRepo.save(to);
// メソッドが正常終了 → 自動コミット
// RuntimeException がスロー → 自動ロールバック
}
}
トランザクション伝播
// REQUIRED(デフォルト):既存トランザクションに参加、なければ新規作成
@Transactional(propagation = Propagation.REQUIRED)
// REQUIRES_NEW:常に新規トランザクションを作成し、現在のトランザクションをサスペンド
@Transactional(propagation = Propagation.REQUIRES_NEW)
// SUPPORTS:既存トランザクションに参加、なければ非トランザクションで実行
@Transactional(propagation = Propagation.SUPPORTS)
// NOT_SUPPORTED:非トランザクションで実行、現在のトランザクションがあればサスペンド
@Transactional(propagation = Propagation.NOT_SUPPORTED)
// NEVER:トランザクション内での実行を禁止、あれば例外をスロー
@Transactional(propagation = Propagation.NEVER)
// MANDATORY:トランザクション内での実行が必須、なければ例外をスロー
@Transactional(propagation = Propagation.MANDATORY)
// NESTED:ネストされたトランザクション(セーブポイント使用)。外側のロールバックで内側もロールバック、内側のロールバックは外側に影響しない
@Transactional(propagation = Propagation.NESTED)
分離レベル
| 分離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
|---|---|---|---|
| READ_UNCOMMITTED | ✅ あり | ✅ あり | ✅ あり |
| READ_COMMITTED | ❌ なし | ✅ あり | ✅ あり |
| REPEATABLE_READ | ❌ なし | ❌ なし | ✅ あり |
| SERIALIZABLE | ❌ なし | ❌ なし | ❌ なし |
@Transactional(isolation = Isolation.READ_COMMITTED) // Oracle のデフォルト
@Transactional(isolation = Isolation.REPEATABLE_READ) // MySQL InnoDB のデフォルト
@Transactional(isolation = Isolation.SERIALIZABLE) // 最も厳格、パフォーマンスが最低
注意事項
@Service
public class UserService {
// ❌ 自己呼び出しではトランザクションが発動しない(this はプロキシではないため AOP が機能しない)
public void outer() {
this.inner();
}
@Transactional
public void inner() { ... }
// ✅ コンテナからプロキシオブジェクトを取得して呼び出す
@Autowired
private ApplicationContext context;
public void outer2() {
context.getBean(UserService.class).inner();
}
// ❌ デフォルトでは RuntimeException のみロールバック、チェック例外はロールバックしない
@Transactional
public void riskyOp() throws IOException {
// IOException はデフォルトでロールバックしない
}
// ✅ 全例外でロールバックするよう明示的に指定
@Transactional(rollbackFor = Exception.class)
public void safeOp() throws IOException { ... }
// ❌ private メソッドの @Transactional は無効(AOP は private メソッドをプロキシできない)
@Transactional
private void internalUpdate() { ... }
}
Spring Data JPA
Repository 層
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// メソッド命名規則 — Spring が実装を自動生成
List<User> findByEmail(String email);
List<User> findByAgeGreaterThan(int age);
Optional<User> findByEmailAndStatus(String email, UserStatus status);
boolean existsByEmail(String email);
long countByStatus(UserStatus status);
// ページングとソート
Page<User> findByStatus(UserStatus status, Pageable pageable);
List<User> findAll(Sort sort);
// カスタム JPQL
@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> search(@Param("keyword") String keyword);
// ネイティブ SQL
@Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)
List<User> findCreatedAfter(@Param("date") LocalDate date);
// 更新 / 削除操作には @Modifying が必要
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") UserStatus status);
}
エンティティ定義
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_email", columnList = "email", unique = true)
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
// 遅延ロード — N+1 問題を回避
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
}
よくある問題とベストプラクティス
循環依存
Spring Boot 2.6+ はデフォルトで循環依存を禁止しており、検出された場合は BeanCurrentlyInCreationException をスローします。
// ❌ 循環依存(A は B に依存し、B は A に依存する)
@Service class A { @Autowired B b; }
@Service class B { @Autowired A a; }
// ✅ 解決策 1:設計を見直し、共通ロジックを新しいクラス C に抽出(推奨)
@Service class C { /* 共通ロジック */ }
@Service class A { @Autowired C c; }
@Service class B { @Autowired C c; }
// ✅ 解決策 2:@Lazy 遅延注入で初期化のデッドロックを回避
@Service class A {
@Autowired @Lazy B b;
}
// ✅ 解決策 3:セッター注入(根本的な解決ではなく回避策のため非推奨)
@Service class A {
private B b;
@Autowired
public void setB(B b) { this.b = b; }
}
複数の Bean から選択する
// 2 つの UserRepository 実装
@Component("mysqlUserRepo")
public class MysqlUserRepository implements UserRepository { ... }
@Component("redisUserRepo")
public class RedisUserRepository implements UserRepository { ... }
// 方法 1:@Qualifier で名前を指定
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;
// 方法 2:@Primary でデフォルト実装をマーク
@Primary
@Component
public class MysqlUserRepository implements UserRepository { ... }
// 方法 3:全実装を注入して自分で選択
@Autowired
private List<UserRepository> allRepos;
@Autowired
private Map<String, UserRepository> repoMap; // キーは bean 名
よくある落とし穴
// 1. private メソッドの @Transactional は無効
@Transactional // ❌ 無効 — AOP は private メソッドをプロキシできない
private void internalUpdate() { ... }
// 2. static メソッドのアノテーションは無効(AOP はインスタンスプロキシに依存)
@Cacheable("users") // ❌ 無効
public static User staticGet(Long id) { ... }
// 3. prototype Bean を singleton に注入すると常に同じインスタンスが返る
// ✅ @Lookup を使うと Spring が毎回呼び出し時に新しい prototype を生成する
@Component
public class SingletonBean {
@Lookup
public PrototypeBean getPrototype() {
return null; // Spring がこのメソッドをオーバーライドし、毎回新しいインスタンスを返す
}
}
// 4. Session クローズ後に JPA の遅延ロード関連にアクセスすると LazyInitializationException
// ✅ 解決策:Service 層で @Transactional を使って Session を維持するか、JOIN FETCH を使用
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
Fonnpo