SpringFramework のアノテーションで List や Map に外部から値を注入する

Spring の Bean に、 List や Set, Map の値を外部から注入したいという状況はしばしば発生すると思います。典型的なパターンのサンプルコードは以下のようなものでしょう。

public class SampleBean {
    private List<String> fruitsList; // ここに注入したい
    public void setFruits(List<String> fruitsList) {
        this.fruitsList = fruitsList
    }
    
    public void showFruits {
        for (String fruit : fruitsList) {
            System.out.println("fruit: " + fruit);
        }
    }
}
<bean id="sampleBean" class="sandbox.spring.FooBean">
    <property name="fruits">
        <list>
            <value>apple</value>
            <value>banana</value>
            <value>orange</value>
        </list>
    </property>
</bean>


さて、アノテーション時代の Spring Bean では、当然、以下のように書きたいわけです。

@Service
public class SampleBean {
    @Resource
    private List<String> fruitsList;

    public void showFruits() {
        for (String fruit : fruitsList) {
            System.out.println("fruit: " + fruit);
        }
    }


ここで問題になるのは fruitsList への値のバインド方法です。注入する値は設定ファイルに書くとして、

  • どういった形式で書けばよいのか
  • それをどうやってバインドするのか

といったあたりが不明な点ですが、実はこれは ListFactoryBean, SetFactoryBean, MapFactoryBean といった、コレクション向けに用意されている FactoryBean を使うことで解決できます。

    <context:component-scan base-package="sandbox.spring" />

    <bean id="fruitsList"
          class="org.springframework.beans.factory.config.ListFactoryBean">
        <property name="sourceList">
            <list value-type="java.lang.String">
                <value>apple</value>
                <value>banana</value>
                <value>orange</value>
            </list>
        </property>
    </bean>


なお、Spring 2.0 以降で導入された xsd の util 名前空間を使用するともう少しスマートに書けます。

    <util:list id="fruitsList" value-type="java.lang.String">
        <value>apple</value>
        <value>banana</value>
        <value>orange</value>
    </util:list>


気持ち短くなる程度ですが、ふだんあまり使わない という記法を使用しているため、視覚的にはずいぶん違って見えますね。
さて、これら List, Set, Map をアノテーションを使ってバインドするときにひとつ注意しなければならないことがあります。それは 「@Autowired アノテーションを使わない」こと。@Autowired アノテーションを使うと、もれなく NoSuchBeanDefinitionException が飛んできます。@Qualifier で名前を指定してもダメです。ここでは @Resource アノテーションを使用します。名前を指定したい場合は @Resource(name = "hoge") とします。

    // @Autowired @Qualifier("prefsList") は NG
    @Resource(name = "prefsList")
    private List<String> prefs;

    public void showPrefs() {
        for (String pref : prefs) {
            System.out.println("pref: " + pref);
        }
    }


これは Spring の公式ドキュメントにも記述があります。少し長いですが以下に引用します(太字は引用者強調)。

Tip
If you intend to express annotation-driven injection by name, do not primarily use @Autowired, even if is technically capable of referring to a bean name through @Qualifier values. Instead, use the JSR-250 @Resource annotation, which is semantically defined to identify a specific target component by its unique name, with the declared type being irrelevant for the matching process.
As a specific consequence of this semantic difference, beans that are themselves defined as a collection or map type cannot be injected through @Autowired, because type matching is not properly applicable to them. Use @Resource for such beans, referring to the specific collection or map bean by unique name.
@Autowired applies to fields, constructors, and multi-argument methods, allowing for narrowing through qualifier annotations at the parameter level. By contrast, @Resource is supported only for fields and bean property setter methods with a single argument. As a consequence, stick with qualifiers if your injection target is a constructor or a multi-argument method.

http://static.springsource.org/spring/docs/3.0.x/reference/beans.html#beans-factory-extension-factorybean


以上になります。List/Set/MapFactoryBean を使用する例が日本語圏にはあまりないようなので、今回記事を書いてみました。@Qualifier アノテーションについては、本題から逸れますのでまた別の機会に書くことにします。

今回使用したコード

gist に上げてあります。→ https://gist.github.com/1003666