SpringFramework - 相互参照

SpringFrameworkでは管理対象のBeanオブジェクトをXMLファイルを使用して定義します。
このとき、Bean間の関連を指定することで、SpringFrameworkがBeanオブジェクト生成時に関連を解決してくれます。
蛇足ですが、この関連を解決するというのがよく目にするキーワードである「依存性注入(DependencyInjection)」に当たります。
DependencyInjectionなんて格好の良い名前がついていますが、やっていること自体は普通に自分でプログラムを書いた場合に、ソースコード中にハードコードしている処理(Aのオブジェクトを生成後、Aのオブジェクトが使用するBのオブジェクトをセッター等で設定してやること等)と大差ないです。
ただ、そのようなハードコードしている処理やコンポーネントの管理をコンテナ(SpringFrameworkのこと)に任せて設定をXMLファイル等で一元管理できることや、コンテナがコンポーネントを管理しているため、アプリケーションコードにコンポーネントのオブジェクトを渡す前に何らかの処理を埋め込んだりできることが優れている点だと思います。


さてそのようなSpringFrameworkですが、管理するコンポーネントが多くなり各コンポーネント間で関連が増えていった場合に、コンポーネント間での相互参照が発生する可能性があります。
SpringFrameworkでは相互参照を解決できるのか気になったので、以下の3通りを試してみました。

1.setter-based での設定
2.constructor-based での設定
3.setter-based + constructor-based での設定


1.setter-based*1での設定

以下、実験対象のBeanクラス(Foo.java)

public class Foo {
  private String name;
  private Foo other;

  public Foo() {
  }

  public Foo( String name ) {
    this.name = name;
  }

  public Foo( String name, Foo other ) {
    this.name = name;
    this.other = other;
  }

  public void setName( String name ) {
    this.name = name;
  }
  public String getName() {
    return name;
  }

  public void setOther( Foo other ) {
    this.other = other;
  }
  public Foo getOther() {
    return other;
  }

  public String toString() {
    return super.toString() + "(name: "+name+", other: "+other.getName()+")";
  }
}

Bean定義ファイル(beans.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
                       "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <bean id="foo1" class="Foo">
    <property name="name"><value>foo1</value></property>
    <property name="other"><ref local="foo2"/></property>
  </bean>

  <bean id="foo2" class="Foo">
    <property name="name"><value>foo2</value></property>
    <property name="other"><ref local="foo1"/></property>
  </bean>
</beans>

利用側クラス

import java.io.FileInputStream;
import org.springframework.beans.factory.xml.XmlBeanFactory;

public class Main {
  public static void main( String[] args ) throws Exception {
    XmlBeanFactory factory = new XmlBeanFactory( new FileInputStream( "beans.xml " ) );
    Foo foo1 = (Foo)factory.getBean( "foo1" );
    Foo foo2 = (Foo)factory.getBean( "foo2" );
    System.out.println( "foo1: ["+foo1+"]" );
    System.out.println( "foo2: ["+foo2+"]" );
  }
}


上記の実行結果は以下のようになり、相互参照は無事解決できました。

foo1: [Foo@1bab50a(name: foo1, other: foo2)]
foo2: [Foo@c3c749(name: foo2, other: foo1)]


2.constructor-based*2での設定

できるわけが無いのですが、次にconstructor-basedでも試してみます。
Foo.javaとMain.javaはsetter-basedでの物をそのまま使います。


Bean定義ファイル(beans.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
                       "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <bean id="foo1" class="Foo">
    <constructor-arg index="0" type="java.lang.String"><value>foo1</value></constructor-arg>
    <constructor-arg index="1" type="Foo"><ref local="foo2"/></constructor-arg>
  </bean>

  <bean id="foo2" class="Foo">
    <constructor-arg index="0" type="java.lang.String"><value>foo2</value></constructor-arg>
    <constructor-arg index="1" type="Foo"><ref local="foo1"/></constructor-arg>
  </bean>
</beans>

案の定、例外が発生しました(無駄に長いっすね)。

Exception in thread "main" org.springframework.beans.factory.BeanCreationExcepti
on: Error creating bean with name 'foo1' defined in resource for InputStream: Ca
n't resolve reference to bean 'foo2' while setting property 'constructor argumen
t with index 1'; nested exception is org.springframework.beans.factory.BeanCreat
ionException: Error creating bean with name 'foo2' defined in resource for Input
Stream: Can't resolve reference to bean 'foo1' while setting property 'construct
or argument with index 1'; nested exception is org.springframework.beans.factory
.BeanCurrentlyInCreationException: Error creating bean with name 'foo1': Request
ed bean is already currently in creation
org.springframework.beans.factory.BeanCreationException: Error creating bean wit
h name 'foo2' defined in resource for InputStream: Can't resolve reference to be
an 'foo1' while setting property 'constructor argument with index 1'; nested exc
eption is org.springframework.beans.factory.BeanCurrentlyInCreationException: Er
ror creating bean with name 'foo1': Requested bean is already currently in creat
ion
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creati
ng bean with name 'foo1': Requested bean is already currently in creation
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean
(AbstractBeanFactory.java:154)
                :
                :

foo1をインスタンス化するときにfoo2が必要になり、foo2をインスタンス化するにはfoo1が必要であり・・・となってしまい処理に失敗して例外が発生します。


3.setter-based + constructor-based

じゃぁってことで、nameプロパティはコンストラクタで、otherプロパティはセッターで設定すればできるだろうと思って試してみました。
setter-basedで初期化を行なう場合は問題なく処理できるので、出来そうな気がするのですが・・・


Bean定義ファイル(beans.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
                       "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <bean id="foo1" class="Foo">
    <constructor-arg index="0" type="java.lang.String"><value>foo1</value></constructor-arg>
    <property name="other"><ref local="foo2"/></property>
  </bean>

  <bean id="foo2" class="Foo">
    <constructor-arg index="0" type="java.lang.String"><value>foo2</value></constructor-arg>
    <property name="other"><ref local="foo1"/></property>
  </bean>
</beans>


できました!

foo1: [Foo@1888759(name: foo1, other: foo2)]
foo2: [Foo@6e1408(name: foo2, other: foo1)]


※1.と3.の場合でも、foo1 の depends-on 属性に foo2 を指定すると失敗します。foo2 の depends-on 属性に foo1 を指定する場合は成功します。
この辺はインスタンス化の順番等に関係しているためと思われます。
インスタンス化の順番によって成功/失敗が左右されるような設定やコードは書くべきではないと思うので、depends-onを必要としないクラス設計等で回避したほうがよいと思います。

*1:コンポーネントの初期化をセッターメソッドを使用することで行なう

*2:コンポーネントの初期化をコンストラクタ引数を使用して行なう