Springの依存性の注入(Dependency Injection:DI)
まずは、SpringのDIの方式を一回振り返って見ます。
SpringのDIは@Autowiredアノテーションで実現していますので、@AutowiredのJavaDocに「Marks a constructor, field, setter method」記述があって、3方式があります。
/**
* Marks a constructor, field, setter method, or config method as to be autowired by
* Spring's dependency injection facilities. This is an alternative to the JSR-330
* {@link javax.inject.Inject} annotation, adding required-vs-optional semantics.
*
* 略
*/
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
/**
* Declares whether the annotated dependency is required.
* <p>Defaults to {@code true}.
*/
boolean required() default true;
}
Field Injection (非推奨)
イメージとしてはクラスのFieldにAutowired Annotationを付けてDIする
@Component
public class AClass {
@Autowired
private BClass bClass;
}
ソースはきれいになりますが、下記のデメリットがあるのに、推奨ではありません。
- 起動時に循環参照エラーが検知できずに、該当クラスを使うときに循環参照のエラーが検知できます。
- 外部からクラス間の依存性は見えない(※Contrcutor DIとSetter DIはパラメータから見えますが、Field DIはクラス内部のprivate fieldは見えない)
- クラス間のDI順番コントロールし難しい
@Component
public class AClass {
@Autowired
private BClass bClass;
public AClass() {
// ここはNullPointerExceptionが発生する
System.out.println(bClass.toString())
}
}
##Constructor Injection(Spring 4.xから推奨)
イメージとしては依存するクラスをConstructorのParamterとして、Autowired AnnotationをConstructorに付けてDIする
@Component
public class AClass {
private final BClass bClass;
@Autowired
public AClass(BClass bclass) {
this.bClass = bClass;
}
}
- DIしたいクラスが5個以上の場合、Constructorのパラメータが長くなってしまう
- 循環参照は発生する可能性がある
Setter Injection
イメージとしては依存するクラスのSetterメソッドを用意して、該当メソッドにAutowired AnnotationをConstructorに付けてDIする
@Component
public class AClass {
private BClass bClass;
@Autowired
public void setBClass(BClass bclass) {
this.bClass = bClass;
}
}
Spring3.x時に、公式サイトに推奨の書き方ですが、依頼するクラス数が多いときに、setterメソッドはいっぱい用意必要があって、ソースは読みずらいになってしまう
循環参照
循環参照とはクラス間互いに依存することになります。下記の2図示す通り。
クラス間の循環参照
参照は「AClass → BClass → CClass → AClass」になって、循環参照となっています。
単一クラス
循環参照エラー
公式サイトのドキュメントに書きの記載があります
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependency-resolution
コンストラクターでInjectionの場合、循環参照は解決できなくて、代わりに、SetterのInjectionで変更が必要となります。
Circular dependencies
If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.
For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.
One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.
Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).
循環参照の例
- AClassを定義する
@Component
public class AClass {
private final BClass bClass;
@Autowired
public AClass(BClass bClass) {
this.bClass = bClass;
}
}
- BClassを定義する
@Component
public class BClass {
private final CClass cClass;
@Autowired
public BClass(CClass cClass) {
this.cClass = cClass;
}
}
- CClassを定義する
@Component
public class CClass {
private final AClass aClass;
@Autowired
public CClass(AClass aClass) {
this.aClass = aClass;
}
}
- Springを起動する
public class AppStartup {
public static void main(String[] args) {
// load context
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
ctx.getBean(AClass.class);
System.out.println("OK!");
}
}
- 下記のエラーが発生しまいました。
co.jp.daker.AppStartup
11月 14, 2021 12:04:28 午前 org.springframework.context.support.AbstractApplicationContext refresh
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'AClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\AClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'BClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\BClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'CClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\CClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AClass': Requested bean is currently in creation: Is there an unresolvable circular reference?
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'AClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\AClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'BClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\BClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'CClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\CClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AClass': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:93)
at co.jp.daker.AppStartup.main(AppStartup.java:11)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'BClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\BClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'CClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\CClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AClass': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 14 more
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'CClass' defined in file [D:\WorkDir\git_dir\spring-ioc\target\classes\co\jp\daker\component\CClass.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AClass': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 28 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AClass': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 42 more
Process finished with exit code 1
発生の流れを整理する
まずはJava Objectのイスタンス化と初期化を説明します。
public Class SampleObject {
private Map<String, Object> field;
public void setField(Map<String, Object> field) {
this.field = field;
}
}
-
インスタンス化
インスタンス化とは、クラス定義を元に、実体を生成することです。例えば、上記のSampleObjectに対して、new SampleObject()でインスタンスを作成して、fieldなどはまだ初期値(null)となっている。
-
初期化
初期化とは、簡単に言えば中身のからっぽな変数に対し、値を代入することです。例えば、上記のSampleObjectに対して、できたインスタンスでsetFieldを読んで入れたい初期値をfieldに設定する。
1.Aのインスタンスを作成するために、Aのコンストラクターを実行する必要があります
2.上記のAのインスタンスを作成するために、Aのコンストラクターの各パラメータクラスのインスタンスが作成する必要があるので、Bのインスタンス作成はAのインスタンス作成の前提となっている
3.Bのインスタンス作成前提はCのインスタンスを作成する
4.Cのインスタンス作成前提はAのインスタンスを作成する
5.上の作成を整理すると、上記の「クラス間の循環参照」の図となるので、循環参照のエラーが出ました。
Setter DIでエラー解消
上記のコンストラクターDIからSetter DIするように修正したら、エラーが解消できます。
- AClassを定義する
@Component
public class AClass {
private BClass bClass;
public AClass() {
System.out.println("AClass Constructor.");
}
@Autowired
public void setBClass(BClass bClass) {
System.out.println("set BClass.");
this.bClass = bClass;
}
}
- BClassを定義する
@Component
public class BClass {
private CClass cClass;
public BClass() {
System.out.println("BClass Constructor.");
}
@Autowired
public void setCClass(CClass cClass) {
System.out.println("set CClass.");
this.cClass = cClass;
}
}
- CClassを定義する
@Component
public class CClass {
private AClass aClass;
public CClass() {
System.out.println("CClass Constructor.");
}
@Autowired
public void setAClass(AClass aClass) {
System.out.println("set AClass.");
this.aClass = aClass;
}
}
- AppStartupは正常に起動できることが確認できている
AClass Constructor.
BClass Constructor.
CClass Constructor.
set AClass.
set CClass.
set BClass.
OK!
Process finished with exit code 0
Setter DIの流れは下記となる
循環参照エラー発生状況のマトリックス
※クラスAとクラスB互いに依頼するケース(クラスの初期化について、クラスAは先にロードする)
クラス間参照状況 | DI方式 | 循環参照成功可否 |
---|---|---|
AとB循環参照 | 両方ともSetter DIで | 〇 |
AとB循環参照 | 両方ともField DIで | 〇 |
AとB循環参照 | 両方ともConstructor DIで | × |
AとB循環参照 | A中にBはSetter DIで、B中にAはConstructor DIで | 〇 |
AとB循環参照 | A中にBはConstrucotr DIで、B中にAはSetter DIで | × |