Spring DI(Dependency Injection)とBean

Spring Frameworkの根幹は「Dependency Injection(依存性注入)」にあります。これはいったい何なのか。その基本的な使い方を学び、DIの機能を説明します。

DIは「依存性」を分離する

DI(Dependency Injection、依存性注入)とは

Spring Frameworkは「DIコンテナ」というフレームワークとして始まりました。DIは「依存性注入」という機能です。

プログラムでは、さまざまな機能をコンポーネント化して利用することがよくあります。コンポーネントに各種プロパティなどを設定して利用するのです。このとき、細かな設定をすべてコードに書いてしまうと、後からの変更やテストが非常に複雑になります。

このようなコンポーネントの設定など、特定の状況で構成されるものを「依存性」と呼びます。簡単に言えば、オブジェクトを生成または使用するうえで依存関係にある場合のことです。この依存性があるため、コードは特定の状況でしか使えない形になってしまいます。

そこで、コンポーネントの設定情報などの依存性をコードから分離し、外部から注入するようにしよう、というのが「依存性注入」の基本的な考え方です。方法はいくつかありますが、基本は「Beanと設定ファイル」でプログラムを作ると考えると理解しやすいでしょう。

Beanは、さまざまな値などをプロパティとして持つ単純なクラスです。通常はBeanインスタンスを生成し、各種プロパティを設定して使用します。ここで、この設定処理、つまり依存性の部分をコードから分離できれば、コードは単純になり、テストもしやすくなります。

Spring Frameworkは、依存性の部分をXMLファイルに記述しておき、それを読み込んで自動的にBeanインスタンスを生成できます。ほかにもアノテーションを利用する方法がありますが、Bean設定ファイルを利用する方法が最も基本的です。

SpringでのDI構文3種類

フィールド注入

  • Springの @Autowired を利用し、オブジェクト内部のフィールドに宣言して注入する方式です。
  • 手軽ですが、参照関係を目で確認しにくくなります。
  • 多用すると参照関係が複雑になる可能性があります。
public class Foo {
    @Autowired
    private Bar bar;
}

フィールド注入の利点

  • コードが簡潔です。

フィールド注入の欠点

  • unit testが難しくなります。
  • final 宣言ができません。
  • 循環依存性が発生した場合に検出できません。循環依存性とは、A -> B、B -> Aのような状態です。

Setter注入 (Setter Injection : type 2 IoC)

  • Springの @Autowired を利用し、Setterメソッドを通じて注入する方式です。
  • Spring FrameworkのBean設定XMLファイルでは property を使用します。
  • Null Pointer Exceptionが発生する可能性があります。
public class Foo {
    private Bar bar;

    @Autowired
    public void setBar (Bar bar) {
        this.bar = bar;
    }
}

コンストラクター注入 (Constructor Injection)

  • コンストラクターを利用して、クラス間の依存関係を接続します。
  • Spring FrameworkのBean設定XMLファイルでは constructor-arg を使用します。
  • 必須の依存関係をコンストラクター経由で注入する方式であり、final を利用できます。
  • フィールド注入と違い、参照関係を目で簡単に確認できます。
  • 初期生成時に割り当てられる必要があるため、Null Pointer Exceptionは発生しません。
public class Foo {
    private final Bar bar;

    @Autowired
    public Foo(Bar bar) {
        this.bar = bar;
    }
}

コンストラクター注入を使う利点

  • 循環参照を防止できます。
    • 循環参照は、AがBを参照し、BがAを参照する場合に発生する問題です。
    • コンストラクター注入では、Beanを先に生成せず、注入しようとするBeanを探します。そのため、アプリケーション起動時にエラーが発生し、問題をすぐに見つけられます。
  • final 宣言が可能です。
    • コンストラクター注入では、依存性注入がクラスのインスタンス化中に始まるため、final を宣言できます。したがってオブジェクトを不変にできます。
  • 単体テストコードを書きやすくなります。
    • Springコンテナの助けなしに、テストコードをより便利に書けます。

Lombokの @RequiredArgsConstructor を使う

コンストラクター注入方式では、注入するオブジェクトが変わるたびにコンストラクターコードを修正する必要があります。

この手間を解決する方法として、Lombokの @RequiredArgsConstructor を利用できます。@RequiredArgsConstructor は、final または @NotNull が付いたフィールドのコンストラクターを自動的に作成します。

@RequiredArgsConstructor
public class Foo {
    private final Bar bar;
}

SpringでのDI

仕様書に従って部品を組み立てることに似ています。

  • プログラマーが行うこと
    • 部品を作る、つまりクラスを実装する。
    • 部品の組み立て仕様書を定義する。コンストラクター注入方式がおすすめです。
  • Spring Frameworkが行うこと
    • 部品を組み立てる。つまり、コンストラクターやsetterを自動的に呼び出してDIを行います。
    • 使い方を知っている「製品」は自動的に利用します。
      • 例: 特定URLへのrequestが到着すると、そのrequestに対する処理ロジック(Controller)を自動的に呼び出します。

インターフェースとBeanクラスの作成

それでは、実際に簡単な例を作りながらDIの基本を説明していきます。まずBeanクラスを作成します。今回は1つのメッセージを保持する単純なBeanを用意します。

前回作成したプロジェクト MySpringAppcom.devkuma.spring パッケージ配下に、次のインターフェースとクラスを作成します。

SampleBeanInterface はBeanの内容を定義するインターフェースです。ここにはメッセージをやり取りする getMessagesetMessage の2つのメソッドだけを用意します。

SampleBeanInterfaceインターフェース

package com.devkuma.spring;
 
public interface SampleBeanInterface {
    public String getMessage();
    public void setMessage(String message);
}

これを実装したクラスが SampleBean です。message というStringのプロパティと、toString メソッドのオーバーライドを持っています。特別な機能はない単純なBeanです。

「こんなに簡単なものなのに、なぜインターフェースから作らなければならないのか」と思うかもしれません。Spring FrameworkのBean利用では、別途インターフェースを作らなくても使用できます。ただし、Beanの一般的な使い方をイメージしやすくするため、今回はインターフェースから作成しました。

SampleBeanクラス

package com.devkuma.spring;
 
public class SampleBean implements SampleBeanInterface {
    private String message;
     
    public SampleBean() {
        message = "(no message)";
    }
     
    public SampleBean(String message) {
        this.message = message;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    @Override
    public String toString() {
        return "SampleBean [message=" + message + "]";
    }
}

Bean設定ファイルの作成

次に、Beanを利用するための設定ファイルを作成します。プロジェクトの src/main フォルダーに resources フォルダーを作成し、その中にBean設定ファイルを作ります。

以下はその例です。これを記述し、bean.xml という名前で resources フォルダーに保存します。

<?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="bean1" class="com.devkuma.spring.SampleBean">
        <property name="message" value="Hello, this is Bean Sample!!" />
    </bean>
 
</beans>

このBean設定ファイルは、<beans> タグの中に <bean> タグを使ってBeanの情報を記述します。意味は次のようになります。

<bean id="名前" class="クラス">
    <property name="プロパティ名" value="値"/>
    ...... 必要な数だけ <property> を追加 ......
</bean>

今回の SampleBean には message というプロパティが1つ用意されています。そこで name="message"<property> タグを1つ用意しました。ここにプロパティに設定される値の情報を入れておきます。こうすると、ここに記述されたプロパティ値が設定されたBeanインスタンスを自動的に生成できるようになります。

アプリケーションでBeanを利用する

それでは、bean.xml に定義されたBeanをアプリケーションで利用してみましょう。MySpringAppcom.devkuma.spring パッケージに App.java を作成し、次のようにソースコードを記述します。実行すると SampleBean がprintlnされ、Hello, this is Bean Sample!! と表示されます。

package com.devkuma.spring;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
        SampleBeanInterface bean1 = (SampleBeanInterface)app.getBean("bean1");
        System.out.println(bean1);
    }
 
}

実行結果:

9월 03, 2017 4:38:49 오후 org.springframework.context.support.AbstractApplicationContext prepareRefresh
정보: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5ce65a89: startup date [Sun Sep 03 16:38:49 KST 2017]; root of context hierarchy
9월 03, 2017 4:38:49 오후 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
정보: Loading XML bean definitions from class path resource [bean.xml]
9월 03, 2017 4:38:50 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
정보: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@79b4d0f: defining beans [bean1]; root of factory hierarchy
SampleBean [message=Hello, this is Bean Sample!!]

実行内容を簡単に説明します。

1. Bean設定ファイルからApplicationContextを生成する。

ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");

Bean利用の基本は、まず ApplicationContext というクラスのインスタンスを取得することです。このクラスは名前のとおり、アプリケーションのコンテキストを管理します。この場合のコンテキストは、ひとまずBeanだと考えてもよいでしょう。

ApplicationContext を作成する方法はいくつかありますが、その1つがBean設定ファイル、ここでは先ほど作成した bean.xml を読み込み、それをもとに作成する方法です。Bean設定ファイルから生成される ApplicationContextClassPathXmlApplicationContext というクラスになります。これはXMLファイルを処理する機能が追加されたクラスです。引数にはBean設定ファイル名を指定します。

2. Beanを取得する。

SampleBeanInterface bean1 = (SampleBeanInterface) app.getBean("bean1");

ApplicationContext インスタンスが準備できたら、あとは簡単です。getBean メソッドを呼び出すだけです。これは引数に指定した名前のBeanインスタンスを取り出すものです。bean.xml を作成したときに <bean id="bean1"...> と書いたことを思い出してください。この id で指定された値が getBean の引数に使われます。

このように取り出されたBeanは、通常のインスタンスと同じように使用できます。注目すべき点は、Beanにはすでに message プロパティの値が設定されていることです。bean.xml には <property> タグを記述しています。

これは、bean.xml の値を変更するだけで、ソースコードをまったく変更せずに使用する SampleBean の内容を変えられるということです。これが「依存性注入」です。言い換えると、Beanを使用するコードに一切変更を加えず、外部からBeanの内容を操作できるということです。

別のBeanを追加する

これで依存性注入の基本的な仕組みは分かりました。もう一歩進んで、別のBeanを作成して利用してみましょう。

次のような簡単なサンプルを作成します。今回は SomeBean というクラスを作ります。これも SampleBeanInterface をimplementsし、message プロパティを持ちます。しかし実際には、String型の message フィールドは存在しません。内部には DateSimpleDateFormat をフィールドとして保持しておき、日時を一時的にテキストとして message でやり取りできるようにしています。

package com.devkuma.spring;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class SomeBean implements SampleBeanInterface {
  private Date date;
  private SimpleDateFormat format;

  public SomeBean() {
   date = Calendar.getInstance().getTime();
    format = new SimpleDateFormat("yyyy/MM/dd");
  }

 public String getMessage() {
    return format.format(date);
 }

 public void setMessage(String message) {
    try {
     date = format.parse(message);
   } catch (ParseException e) {
      e.printStackTrace();
      date = null;
    }
 }

 @Override
 public String toString() {
    return "SomeBean [date=" + format.format(date) + "]";
 }
}

クラスを用意したら、bean.xml を開き、先ほど記述した <bean> タグ部分を次のように修正します。

<bean id="bean1" class="com.devkuma.string.SomeBean">
    <property name="message" value="2017/9/3"/>
</bean>

これで実行すると、出力されるテキストが SomeBean [date=2017/09/03] に変わります。SomeBean インスタンスが生成され、利用できるようになっていることが分かるでしょう。Appのソースコードには一切手を加えていないにもかかわらずです。

9월 03, 2017 4:56:10 오후 org.springframework.context.support.AbstractApplicationContext prepareRefresh
정보: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5ce65a89: startup date [Sun Sep 03 16:56:10 KST 2017]; root of context hierarchy
9월 03, 2017 4:56:10 오후 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
정보: Loading XML bean definitions from class path resource [bean.xml]
9월 03, 2017 4:56:10 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
정보: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@79b4d0f: defining beans [bean1]; root of factory hierarchy
SomeBean [date=2017/09/03]

この例のように、インターフェースを定義して実装クラスを複数用意しておけば、単にプロパティ値を設定するだけでなく、そのプロパティの処理方法なども自由に変更できるようになります。使用するクラスやプロパティ値は、コードをまったく変更せずに変えられます。これで「Beanインスタンスを設定ファイルから自動的に生成する」という方式の利点が分かりました。