Spring Boot のバージョンを上げたところ、コードは何も変えていないのに、起動時に次のエラーが出るようになりました。
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| inventoryService (field private com.example.demo.OrderService com.example.demo.InventoryService.orderService)
↑ ↓
| orderService (field private com.example.demo.InventoryService com.example.demo.OrderService.inventoryService)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
結論:Spring Boot 2.6 から、Bean 同士の循環参照が既定で禁止されました。 2.5 までは Spring が自動で循環を解決していたので、昔から循環していたコードがバージョンアップで表に出ます。設定で以前の動きに戻せますが、本来は循環そのものをなくします。
躓いたポイント
環境
| 項目 | バージョン |
|---|---|
| OS | Windows 11 |
| JDK | Eclipse Temurin 17.0.20(2.5 / 2.6)、21.0.12(3.5 / 4.1) |
| Spring Boot | 2.5.15、2.6.0、3.5.16、4.1.1 |
| Maven | 3.9.16 |
起きたこと
注文のサービスが在庫のサービスを使い、在庫のサービスも注文のサービスを使っていました。どちらもフィールドに @Autowired を付けて注入しています。
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
public void order(String item) {
inventoryService.reserve(item);
}
public boolean isOrdered(String item) {
return true;
}
}
@Service
public class InventoryService {
@Autowired
private OrderService orderService;
public void reserve(String item) {
// 在庫側から注文側を呼び返している
orderService.isOrdered(item);
}
}
同じコードを、Spring Boot のバージョンだけ変えて起動した結果です。
| Spring Boot | 結果 |
|---|---|
| 2.5.15 | 起動する |
| 2.6.0 | 起動しない(上のエラー) |
| 3.5.16 | 起動しない(上のエラー) |
| 4.1.1 | 起動しない(上のエラー) |
業務システムでは、サービスクラスが増えるうちに、気付かないまま循環ができていることがよくあります。2.5 までは問題なく動いていたので、2.6 以降へのバージョンアップで初めて見つかります。
どうやって解決したか
応急処置:設定で 2.5 までの動きに戻す
application.properties に次の1行を足すと、2.5 までと同じように起動します。
spring.main.allow-circular-references=true
2.6.0、3.5.16、4.1.1 のいずれでも、この設定で起動することを確認しました。ただしエラーメッセージに「As a last resort(最後の手段として)」とあるとおり、バージョンアップを先に進めるための一時的な手段です。
本来の直し方:循環をなくす
InventoryService が OrderService を呼んでいるのは isOrdered の1か所だけでした。この照会の処理を別のクラスに切り出し、両方のサービスがそれを使う形にします。
// 両方から使われる処理を切り出したクラス。他のサービスには依存しない
@Service
public class OrderQueryService {
public boolean isOrdered(String item) {
return true;
}
}
@Service
public class InventoryService {
private final OrderQueryService orderQueryService;
public InventoryService(OrderQueryService orderQueryService) {
this.orderQueryService = orderQueryService;
}
public void reserve(String item) {
orderQueryService.isOrdered(item);
}
}
@Service
public class OrderService {
private final InventoryService inventoryService;
public OrderService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
public void order(String item) {
inventoryService.reserve(item);
}
}
依存の向きが「注文 → 在庫 → 照会」の一方通行になり、設定を足さなくても 3.5.16 と 4.1.1 で起動しました。
あわせて、フィールドの @Autowired をコンストラクタでの注入に変えています。コンストラクタ注入同士の循環は、allow-circular-references=true にしても起動しません。
Error creating bean with name 'inventoryService': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
設定で隠せないので、循環を作り込んだ時点で必ず気付けます。
解決できた根拠
Spring Boot 2.6 で循環参照が既定で禁止された
Spring Boot 2.6 のリリースノートに「Circular References Prohibited by Default」という節があります。
Circular references between beans are now prohibited by default.
(Bean 間の循環参照は、既定で禁止されるようになりました)
同じ節に、依存の循環をなくすよう設定を見直すこと、それができない場合は spring.main.allow-circular-references を true にすると 2.5 の動きに戻ることが書かれています。
コンストラクタ注入同士の循環は解決できない
Spring Framework のリファレンスの「Circular dependencies」に、次のように書かれています。
If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.
(主にコンストラクタ注入を使っていると、解決できない循環依存ができることがあります)
同じページでは、セッター注入なら循環を構成できるものの「推奨されない(although it is not recommended)」としています。また、必須の依存はコンストラクタで注入することを目安として勧めています。
it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.