Spring FrameworkのDIコンテナについて解説【知らないと損】

Spring FrameworkのDIコンテナについて解説【知らないと損】

Spring FrameworkのDIについて知りたい人「JavaのフレームワークでSpringがよく使われていると聞きました。自分で調べてみたけどDIについてよくわかりません。DIが可能だとなにがいいんだろう?Spring FrameworkのDIについて知りたいです、教えて下さい。」


そんな方向けに解説します。

補足情報:そもそもDIとはなにか?

DIとは「Dependency Injection(依存性の注入)」の訳ですが、この言葉だけ聞いても何のことか全くわからないと思います。

一番わかりやすく言い換えるならば、「あるクラスに必要となるまた別のクラスのインスタンスを設定すること」かなと思います。

また、Spring FrameworkにおけるDIについての話はいろんな所で耳にしますが、本記事の対象である「DIコンテナ」のことを指すと考えて間違いありません。
つまり、DIコンテナを理解することがSpring FrameworkのDIについて理解することと同意だと考えます。

まだピンときていない人もいるかと思いますが、本記事を読んで少しでも理解できるようになれれば幸いです。

それでは本題に入ります。

Spring FrameworkのDIコンテナについて解説

Spring FrameworkのDIコンテナはインスタンスを組み立ててくれる基盤です。
Javaのインスタンスを管理し、管理されているインスタンスに依存するクラスを自動でDIする機能をもちます。

※SpringではDIコンテナが管理するインスタンスのことを「コンポーネント」と表現します。(これ以降では「コンポーネント」と表記)

DIコンテナの使い方の手順は以下になります。

  1. DIコンテナの設定
  2. DIコンテナからコンポーネントを取得


次の章で説明します。

【手順①】DIコンテナの設定

まずはじめに、DIコンテナにインスタンスを管理させるためには以下の設定が必要になります。

  1. Bean定義
  2. DIの記述


順に説明します。

【設定①】Bean定義

SpringではDIコンテナに登録するコンポーネントのことを「Bean」、Beanの構成を定義することを「Bean定義」と言います。

Bean定義方法には以下の3つがあります。

  • XMLベースの定義
  • Javaベースの定義
  • アノテーションベースの定義


順に説明します。

【定義①】XMLベースの定義

XMLベースの定義はSpringが登場してから利用可能な定義方法です。

Bean定義をXMLファイルに記述します。記述方法は以下です。

  1. <beans>要素にBean定義を複数記述します。
  2. <beans>要素の子要素の<bean>要素に個々のBean定義を記述します。


詳細は以下です。

メリット・デメリットプログラムを一切変更することなく、設定の変更のみでアプリケーションの動作を変更できる。
すべてのコンポーネントのBean定義を行う必要があり、手間がかかるためアノテーションベースの定義と合わせつ使うのが一般的である。
Bean名id属性で指定した値がbean名となる。
Beanインスタンスclass属性で指定したクラス
※class属性は完全修飾クラス名を記述する。


例えば以下のように定義します。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" />
</beans>


上記の例ではcom.example.spring.demo.SampleクラスのBeanを定義してます。Bean名は「sample」を指定してます。

【定義②】Javaベースの定義

Javaベースの定義方法はSpring 3.0から利用可能な定義方法です。

Bean定義をJavaのクラスに記述します。記述方法は以下です。

  1. クラスに@Configurationアノテーションを付与し、Bean定義であることを宣言します。(複数のJavaベースの定義クラスを作成することができます。)
  2. @Configurationアノテーションを付与したクラスのメソッドに@Beanアノテーションを付与し、Bean定義を記述します。


詳細は以下です。

メリット・デメリットすべてのコンポーネントのBean定義を行う必要があり、手間がかかるためアノテーションベースの定義と合わせつ使うのが一般的である。
Bean名デフォルトでは@beanアノテーションを付与したメソッド名がbean名となる。指定したい場合は、@Bean(name = “別名”)とする。
Beanインスタンス@beanアノテーションを付与したメソッドの戻り値


例えば以下のように定義します。

package com.example.spring.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean("sample")
    Sample sample(){
        return new Sample();
    }
}


上記の例ではcom.example.spring.demo.AppConfigクラスをBean定義クラスとしてます。また、Bean名が「sample」のBeanを定義してます。

【定義③】アノテーションベースの定義

アノテーションベースの定義方法はSpring 2.5から利用可能な定義方法です。

Bean定義ファイルに記述するのではなく、DIコンテナに管理させたいBean(もしくはコンポーネント)にアノテーションを付与し、スキャン(読み込む)することでDIコンテナに登録します。記述方法は以下です。

  1. Beanクラスに@Componentをアノテーションを付与して、コンポーネントスキャンの対象にする。
  2. (Javaベースの定義の場合)Bean定義クラスに@ComponentScanアノテーションを付与して、指定したパッケージ(※)以下のコンポーネント(1.で@Componentアノテーションを付与したクラス)を自動でスキャンしてDIコンテナに登録する
    ※パッケージの指定方法:@ComponentScan(“パッケージ”)もしくは@ComponentScan(basePackages = “パケージ”)
    ※デフォルトはBean定義クラスと同じパッケージをスキャンする
  3. (XMLベースの定義の場合)Bean定義ファイルにbeans要素の子要素に<context:component-scan>要素を追加。base-package属性(value属性)にスキャン対象のパッケージを指定する。


詳細は以下です。

Bean名デフォルトでは@Componentを付与したクラス名の先頭を小文字にしたものになる。指定したい場合は@Component(“別名”)とする。
Beanインスタンス@Componentを付与したクラス


例えば以下のように定義します。

【コンポーネントスキャン対象のクラス】

package com.example.spring.demo;

import org.springframework.stereotype.Component;

/**
 * @Componentアノテーションを付与することでスキャン対象にしてます。
 * Sapmle2クラスのBean名をデフォルトの値ですが「sample2」として明記してます。
 **/
@Component("sample2")
public class Sample2 {

    String test;

    public Sample2(){
        test = "sample2";
    }

    public void echo(){
        System.out.println(test);
    }
}

【Javaベースの定義の場合】

package com.example.spring.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @ComponentScanアノテーションを付与してコンポーネントスキャンを有効にしてます。
 **/
@Configuration
@ComponentScan
public class AppConfig {
    @Bean("sample")
    Sample sample1(){
        return new Sample();
    }
}

【XMLベースの定義の場合】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 
        <context:component-scan>要素を追加することでコンポーネントスキャンを有効にしてます。
    -->
    <context:component-scan base-package="com.example.spring.demo"/>
    <bean id="sample" class="com.example.spring.demo.Sample" />
</beans>

【設定②】DIの記述

DIの記述方法の種類は以下の3つがあります。

  • セッターインジェクション
  • コンストラクタインジェクション
  • フィールドインジェクション


順に説明します。

【インジェクション①】セッターインジェクション

前述したBean定義の中で、Javaベースの定義、XMLベースの定義、アノテーションベースの定義で利用可能です。

セッターインジェクションはコンポーネントのセッターの引数に依存するコンポーネントを注入する方法です。

アノテーションベースの定義を使用する場合は後述するオートワイヤリング機能を使用します。

例えば以下のように記述します。

【Sample2クラスのセッターの引数がSampleクラスの場合】

package com.example.spring.demo;

public class Sample2 {

    Private Sample sample;

    public Sample2(){
    }

 public void setSample(Sample sample){
        this.sample = sample;
    }
}

【XMLベースのBean定義を使用する場合】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" />
    <bean id="sample2" class="com.example.spring.demo.Sample2">
        <!-- 
            セッターインジェクションを利用する場合はproperty要素を使用する。
            name属性にはセッターのプロパティ名(メソッド名のset以降の文字列※先頭は小文字)を指定します。
            ref属性にはセッターの引数のDIしたいBean名を指定します
            セッターの引数が文字列や数値の場合はref属性ではなくvalue属性で値を指定する。例:<property name="num" value="1"/>
        -->
        <property name="sample" ref="sample"/>
    </bean>
</beans>

【JavaベースのBean定義を使用する場合】

package com.example.spring.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean("sample")
    Sample sample(){
        return new Sample();
    }

    @Bean("sample2")
    Sample2 sample2(){
        Sample2 sample2 = new Sample2();
        // Bean定義メソット「sample()」をセッターの引数に指定してDIしています
        sample2.setSample(sample());
        return sample2;
    }
}

【アノテーションベースのBean定義を使用する場合】

package com.example.spring.demo;

public class Sample2 {

    Private Sample sample;

    public Sample2(){
    }

    /**
     * @Autowiredアノテーションをセッターメソッドに付与することで、引数のコンポーネントが自動でDIされます。
     * 引数の前に@Qualifierアノテーションを付与してBean名を指定してます。
     */
    @Autowired
 public void setSample(@Qualifier("sample") Sample sample){
        this.sample = sample;
    }
}

【インジェクション②】コンストラクタインジェクション

セッターインジェクション同様に前述したBean定義の中で、Javaベースの定義、XMLベースの定義、アノテーションベースの定義で利用可能です。

コンストラクタインジェクションはコンポーネントのコンストラクタに依存するコンポーネントを注入する方法です。

アノテーションベースの定義を使用する場合は後述するオートワイヤリング機能を使用します。

以下のメリットとデメリットがあります。

メリットフィールドにfinal修飾子をつけて、不変にできる。セッターインジェクションとフィールドインジェクションにはできない。
デメリット@Resourceアノテーション(後述)が利用できない


例えば以下のように記述します。

【Sample2クラスのコンストラクタの引数がSampleクラスの場合】

package com.example.spring.demo;

public class Sample2 {

    Private Sample sample;

    public Sample2(Sample sample){
        this.sample = sample;
    }
}

【XMLベースのBean定義を使用する場合】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" />
    <bean id="sample2" class="com.example.spring.demo.Sample2">
        <!-- 
            コンストラクタインジェクションを利用する場合は<constructor-arg>要素を使用する。
            <constructor-arg>要素複数記述する場合はコンストラクタの順番に合わせる必要がある。
            ただし、index属性で順番を指定することが可能。
            ref属性にはコンストラクタの引数のDIしたいBean名を指定します。
            セッターの引数が文字列や数値の場合はref属性ではなくvalue属性で値を指定する。
            → 例:<constructor-arg index="1" value="1"/>
        -->
        <constructor-arg index="0" ref="sample"/>
    </bean>
</beans>

【JavaベースのBean定義を使用する場合】

package com.example.spring.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean("sample")
    Sample sample(){
        return new Sample();
    }

    @Bean("sample2")
    Sample2 sample2(){
        // Bean定義メソット「sample()」をコンストラクタの引数に指定してDIしています
        return new Sample2(sample());
    }
}

【アノテーションベースのBean定義を使用する場合】

package com.example.spring.demo;

public class Sample2 {

    Private Sample sample;

    /**
     * @Autowiredアノテーションをコンストラクタに付与することで引数のコンポーネントが自動でDIされます。
     * 引数の前に@Qualifierアノテーションを付与してBean名を指定してます。
     */
    @Autowired
    public Sample2(@Qualifier("sample") Sample sample){
        this.sample = sample;
    }
}

【インジェクション③】フィールドインジェクション

前述したアノテーションベースの定義で利用可能です。

フィールドインジェクションはコンポーネントのフィールドに依存するコンポーネントを「オートワイヤリング(後述)」という仕組みを用いることでインジェクションする方法です。

以下のメリット・デメリットがあります。

メリットコンストラクタ、セッターは不要なので、コード量を少なくすることができる。
デメリットDIコンテナを利用することが前提となる。
修飾子は指定なし(パッケージプライベート)が好ましいらしい。理由は同じパッケージ内ならフィールドを直接参照可能なので、テストケースを作成する際にテスト用のフィールドクラスを作成して、直接設定できるため


また、オートワイヤリングには以下の2種類が存在します。

オートワイヤリングの種類説明
@Autowiredアノテーションを用いる方法・アノテーションを付与したフィールドと同じ型を持つコンポーネントを自動でインジェクションします。

・デフォルトではインジェクションされることが必須なので、同じ型を持つコンポーネントが1つも登録されていない場合は例外が発生します。

・インジェクションが必須ではない場合はrequired属性にfalseを設定すれば例外を回避でき、フィールドの値はnullとなります。※
※required の代わりに、java.util.Optionalの使用も可能

・アノテーションを付与したフィールドと同じ型のコンポーネントがDIコンテナに複数登録されているときは例外が発生するので、フィールドに@Autowiredアノテーションに加え、@Qualifier アノテーションを付与し、bean名を指定することでインジェクションするコンポーネントを指定できます。
また、フィールドに@Qualifierが付与されなかった場合は複数存在するコンポーネントのbean定義のいずれか1つに@Primaryアノテーションを付与することでインジェクションされるコンポーネントを指定できます。

同じインターフェイスのコンポーネントが複数DIコンテナに登録されている場合はまとめてコレクションやマップとして取得可能です。
@Resourceアノテーションを用いる方法アノテーションを付与したフィールド名(フィールドインジェクションのフィールド名)またはプロパティ名(セッターインジェクションの引数名)と一致するDIコンテナに登録されているコンポーネントをインジェクションします。name属性に指定したbean名のコンポーネントを取得することも可能です。※コンストラクタインジェクションでは使用できません。
一方、名前が一致するコンポーネントがDIコンテナに登録されていない場合は型による解決がされます。


例えば以下のように記述します。

【アノテーションベースのBean定義を使用する場合】

package com.example.spring.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("sample2")
public class Sample2 {

    /**
     * @Autowiredアノテーションをフィールドに付与することでフィールドのコンポーネントが自動でDIされます。
     * 引数の前に@Qualifierアノテーションを付与してBean名を指定してます。
     */
    @Autowired
    @Qualifier("sample")
    Sample sample;

    public Sample2(){
    }

    public void echo(){
        sample.echo();
    }
}

上記の例を@Resourceアノテーションで書き換えると以下のようになります。

package com.example.spring.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("sample2")
public class Sample2 {

    /**
     * @Resourceアノテーションをフィールドに付与することでフィールド名と一致するBean名を持つコンポーネントが自動でDIされます。
     * name属性を指定することでDIするBean名も指定可能です。
     */
    @Resource(name="sample")
    Sample sample;

    public Sample2(){
    }

    public void echo(){
        sample.echo();
    }
}

【手順②】DIコンテナからコンポーネントを取得

次にDIコンテナからコンポーネントを取得する方法を説明します。
以下の手順でDIコンテナからコンポーネントを取得します。

  1. DIコンテナにコンポーネントを登録する
  2. DIコンテナからコンポーネントを取得する


順に説明します。

【方法①】DIコンテナにコンポーネントを登録する

org.springframework.context.ApplicationContextインターフェイスを通じてDIコンテナにコンポーネントを登録します。

ApplicationContextインターフェースの実装クラスは以下のようなものがあります。

ApplicationContextインターフェースの実装クラス説明
org.springframework.context.support .ClassPathXmlApplicationContextXMLベースのBean定義を使用する場合に使います。
XMLファイルの所在はクラスパスからの相対パスを指定する。
org.springframework.context .support.FileSystemXmlApplicationContextXMLベースのBean定義を使用する場合に使います。
XMLファイルの所在は作業パスからのパスを指定する相対パスか、絶対パスを指定する。
org.springframework.context.annotation
.AnnotationConfigApplicationContext
JavaベースのBean定義を使用する場合に使います。
Bean定義クラスを直接指定するか、コンポーネントスキャンをする場合はパッケージを指定する。


DIコンテナにコンポーネントを登録する例は以下です。

// クラスパスからの相対パスのBean定義ファイルAppContext.xmlからコンポーネントを登録
ApplicationContext xmlContext1 = new ClassPathXmlApplicationContext("AppContext.xml");

// 作業ディレクトリからの相対パスのBean定義ファイルAppContext2.xmlからコンポーネントを登録
ApplicationContext xmlContext2 = new FileSystemXmlApplicationContext("demo/AppContext2.xml");

// Bean定義クラスを指定してコンポーネントを登録
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);


追記:XMLベースのBean定義ファイルの置き場所と指定方法について以下の記事で詳しくまとめました。気になる方は、ご覧になってみてください。

【手順②】DIコンテナからコンポーネントを取得する。

※DIコンテナからBeanを取得することを「ルックアップ」とも言います。
DIコンテナからコンポーネントを取得する例は以下です。

// 取得したいコンポーネントのクラスを指定する
Sample sample = context.getBean(Sample.class);

// 取得したいコンポーネントのクラスが複数存在する場合はbean名を指定する。
Sample sample = context.getBean("sample", Sample.class);

// bean名を指定して、取得するクラスでキャストする
Sample sample = (Sample)context.getBean("sample");



DIコンテナの使い方を説明しながらDIコンテナについての説明は以上になります。
次はDIコンテナを利用することでのメリットを解説します。

DIコンテナを利用することで得られる4つのメリット

Spring Framework のDIコンテナを使うことで以下のメリットを得ることができます。

  • インスタンスのスコープ管理が可能
  • インスタンスのライフサイクル制御が可能
  • 共通機能を組み込むことが可能
  • テストが簡単になる


順に説明します。

インスタンスのスコープ管理が可能

本記事では詳しくは記載しません。

追記:スコープについて以下の記事で詳しくまとめました。気になる方は、ご覧になってみてください。

インスタンスのライフサイクル制御が可能

本記事では詳しくは記載しません。(別の記事で書く予定です)

共通機能を組み込むことが可能

いわゆるSpring FrameworkのAOP機能を実現します。

本記事では詳しくは記載しません。(別の記事で書く予定です)

テストが簡単になる

インスタンスをDIコンテナに管理させることで、クラス同士が疎結合となり単体テストが簡単になります。

また、Spring Testを使用することでテスティングフレームワーク上でDIコンテナを動かすことが可能なので結合テストも可能になります。

以下の記事に詳しく書いてあります。参考にしてみてください。



少し長くなってしまいましたがSpring FrameworkのDIコンテナについての解説は以上になります。
今後開発していくなかで気になった点などは追記したり、別記事として書いていく予定ですので、他の記事も見てもらうとよりSpring Frameworkについての知識が深まるかと思います。
これからSpring Frameworkを使おうと考えている方の参考になれば幸いです。


人気記事①:
【厳選4冊+α】Spring Framework初心者におすすめな本

人気記事②:現役エンジニアがおすすめするプログラミングスクール5社:無料あり