問題の説明
SpringBoot 3.x バージョンでは、SpringBoot 2.6.x 以降、数多くの変更が行われました。
SpringBoot 2.7.5~x バージョンを使用したことがある開発者は、SpringBoot プロジェクトに循環依存関係が存在すると、プロジェクト起動時に例外エラーが発生することに気付くでしょう。エラーメッセージの内容は、おおよそ「プロジェクトに循環依存関係があり、プロジェクトが正常に起動できない」といったものです。
SpringBoot 2.7.5~x バージョン以降から SpringBoot 3.x では、この問題が存在しています。Springは循環依存関係の問題を扱わなくなり、開発者が自分で解決するか、開発中に循環依存関係を避ける必要があります。
古いバージョンのプロジェクトをアップグレードする場合、循環依存関係のコードをすべて調整しなければならず、注意しないとコードが崩壊して動作しなくなる可能性があります。
循環依存関係問題の概要
※ 開発中にこの問題に直面して解決方法を急いでいる場合は、「解決策」セクションをご覧ください。
循環依存関係の定義
クラスが初期化時に別のクラスのインスタンスを必要とし、その別のクラスが再び最初のクラスのインスタンスを必要とする場合、循環依存関係の問題が発生します。これにより、アプリケーションは正しく初期化および実行できなくなります。Spring Boot はこのような循環依存関係を処理できず、起動時にエラーが発生します。
循環依存関係の例
UserService
クラスが VipService
クラスを注入し、VipService
クラスが再び UserService
クラスを注入する場合、循環依存関係が発生します。
@Service
public class UserService {
@Resource
private VipService vipService;
}
@Service
public class VipService {
@Resource
private UserService userService;
}
このコードは、2.6.0 バージョン以降でデフォルトで Bean 間の循環参照が禁止されているため、上記のコードではエラーが発生します。
解決策
第一の解決策:循環依存を許可する(推奨されない)
Spring はデフォルトで循環依存をサポートしていないため、設定ファイルで循環依存を再度許可する必要があります。
spring:
main:
allow-circular-references: true # 循環依存を許可する
第二の解決策:遅延初期化
@Lazy:この注釈を使用すると、循環依存の問題を解決できます(Bean を注入する必要がある場所にこの注釈を追加します)。
@Lazy // 遅延初期化を使用
@Autowired
private OneService oneService;
第三の解決策:制御反転
@Service
@RequiredArgsConstructor // この注釈は下記で説明します
public class OneServiceImpl implements OneService {
private final ConfigurableListableBeanFactory beanFactory;
// 循環依存の代替
public TwoService getTwoService() {
return beanFactory.getBean(TwoService.class);
}
}
getTwoService()
を使用して、Bean 工場から対応する Bean を取得します。
@Service
@RequiredArgsConstructor // この注釈は下記で説明します
public class TwoServiceImpl implements TwoService {
private final ConfigurableListableBeanFactory beanFactory;
// 循環依存の代替
public OneService getOneService() {
return beanFactory.getBean(OneService.class);
}
}
上記のコードでは、OneService
が TwoService
に依存し、TwoService
が再び OneService
に依存することで循環依存が発生しています。
解決策としては、使用するたびに Bean 工場から取得することで、循環依存を回避できます。
※ @RequiredArgsConstructor
の使用説明
@RequiredArgsConstructor:この注釈は Lombok により提供され、多くの繰り返しの @Autowired コードを削減できます。
注意点:@RequiredArgsConstructor を使用する場合は、final キーワードを使用する必要があります。
クラスに使用することで、@Autowired 注釈の代わりになりますが、注入時には final で定義する必要があります。または @NotNull 注釈を使用します。
@RestController
@RequiredArgsConstructor // @Autowired 注釈の代替
@RequestMapping("/api/v1/one")
public class OneController {
private final OneService oneService; // final キーワードが必要
private final TwoService twoService; // final キーワードが必要
@GetMapping("{id}")
public ResultVo<String> getDetails(@PathVariable("id") Long id) {
return ResultVo.ok(oneService.getDetailsById(id));
}
}
注意点:
1. 変数は必ず final で宣言する必要があります。
2. コンストラクタインジェクションでは、コンテナがパラメータ付きのクラスコンストラクタを呼び出すことで、コンストラクタに基づく依存性注入が完了します。各パラメータは、そのクラスが他のクラスに対して持つ依存関係を示しており、コンテナはコンストラクタを呼び出してこれらの依存関係に対応するインスタンスを注入します。
3. Bean を注入する必要がある場合は、クラス名に @RequiredArgsConstructor を使用することで、多くの @Autowired 注釈を省略できます。
長文をお読みいただき、ありがとうございました。
不足している点があれば、ぜひ教えてくださいね。もし良いと思っていただけたら、褒めてください!
( •̀ ω •́ )y ~