SpringframeworkからはじめるSpringBoot Part.1 - 依存性注入

承前

SpringBootを触れるようになったけど仕組みが全然分からなくて応用できない!から、やっぱり内部が分からないとと思ってSpringframeworkから見直してみました。 誤った情報を広めたいわけではないのですが、それでも理解がおかしい部分はあるかもしれません。その時はコメントください。

この記事に書いてあること

  • springframeworkのDIとは?
  • xmlでDIを定義する
  • 実際に定義したDIが実行されていることを、サンプルコードで確認する

まずDI(依存性注入)

それでは、まずDIからはじめましょう。これがSpringframeworkの根っこです。

そもそもSpringframeworkにとってのDIって何?

DIというものについてのもっと良い情報は世の中に山ほど公開されています。しかしここでは改めて解説させてください。 ちなみに今調べたら他の資料としては猿でも分かる! Dependency Injection: 依存性の注入 - Qiitaなどが見つかりました。猿でも分かるそうなので人類ならばきっと理解できると思います。

Springframeworkの公式ドキュメントでDIについて言及している部分にはこう書いてあります。

IoC is also known as dependency injection (DI). It is a process whereby objects define their dependencies, that is, the other objects they work with, only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.(引用元は公式ドキュメント)

(これは実はIoCについての記述なのですが)IoCは依存性注入(DI)という名前でも知られています。これはオブジェクトが自分の依存性(動作に必要な別のオブジェクト群)を、自分がインスタンス化されたりファクトリーメソッドから返却された後で、コンストラクタやファクトリーメソッドの引数、オブジェクトのインスタンスの変数のみによって定義する手段です。

つまりあるオブジェクトが使う別のオブジェクトを、なにかより柔軟な方法で定義できる、ということのようですね。

その後の方には、

  • 普通はBean(オブジェクト)自身が、自分が使うオブジェクトを規定するはずなのに、ここではDI ContainerがBeanを構築する時にそいつの使うオブジェクトを決める。これって逆転してるよね。だからIoC(Inversion of Control, 制御の逆転)っていうんだよ。
  • SpringframeworkではIoCを実現するためにorg.springframework.beansorg.springframework.contextってパッケージを使うよ。
  • BeanFactoryはオブジェクトの設定を行う基本的な機能を提供して、ApplicationContextでより実用的な機能を提供するよ。
  • ちなみにさらっと出てきてたけどBeanっていうのはIoCコンテナによってインスタンス化され管理されるオブジェクトのことだよ。

みたいなことが書いてあります。どれも目にしたことのある言葉ばかりですね。結構読みやすいのでぜひ一度目を透してみてください。今後ググった時にSpringの公式ドキュメントが出てきた時にビビらずに済むようになることでしょう。

さて、これでDIについてピンと来たでしょうか?私は当時全く何もピンと来ませんでした。 ですから、手を動かしてみましょう。

DIだけ試してみよう

ここで「ではSpringBootのプロジェクトを作ってAPIを作って…」みたいなことをやると、いろいろややこしいものが入りすぎて何がなんだかわからなくなるので、まずはDIだけを試してみましょう。

SpringframeworkでDIを試すには、 spring-context というライブラリを使います。 公式ドキュメントによればspring-contextは、

it is a means to access objects in a framework-style manner that is similar to a JNDI registry.(引用元はこちらの第2段落)

JNDIレジストリみたいなフレームワークのお作法に則ったやり方でオブジェクトにアクセスする手段です。 この「アクセスする手段」こそがDI(依存性注入)というわけですね。 spring-contextはその依存ライブラリに

  • spring-core
  • spring-bean

などを持っています。上で出てきたorg.springframework.beansパッケージが含まれていそうですね。

1.プロジェクトの準備

さて、では本当に手を動かしていきましょう。 まず元となるプロジェクトを作りましょう。

mvn archetype:generate

対話式でgroupIdなどを尋ねられるので好きなように設定してください。終わったらIntelliJなどのIDEに取り込みます。 古めのJUnitの依存が入ってますが使わないので消してあげましょう。

  <!-- pom.xml -->
  </properties>

  <dependencies>
-   <dependency>
-     <groupId>junit</groupId>
-     <artifactId>junit</artifactId>
-     <version>3.8.1</version>
-     <scope>test</scope>
-   </dependency>
  </dependencies>
</project>

src/test配下も使わないので削除しておいてください。

そしてspring-contextの依存を追加してあげましょう。

  <!-- pom.xml -->

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+   <spring.version>4.2.4.RELEASE</spring.version>
  </properties>

  <dependencies>
  </properties>

  <dependencies>
+   <dependency>
+     <groupId>org.springframework</groupId>
+     <artifactId>spring-context</artifactId>
+     <version>${spring.version}</version>
+   </dependency>
  </dependencies>
</project>

これでSpringframeworkのDIが使えるようになりました。

2.Beanオブジェクトの実装

今回は、文字列を標準出力するサービスクラスを用意して、それを呼び出してDIを体験してみましょう。

sayHelloメソッドを定義するHellointerfaceと、それを実装して文字列を標準出力するクラスを2つ、用意しましょう。

package jp.blackawa.examples;

/**
 * 文字列を標準出力するメソッドを定義するインターフェース.
 */
public interface Hello {
    void sayHello();
}
package jp.blackawa.examples;

public class HelloBoys implements Hello {
    public void sayHello() {
        System.out.println("Hello, Boys!");
    }
}
package jp.blackawa.examples;

public class HelloGirls implements Hello {
    public void sayHello() {
        System.out.println("Hello, Girls!");
    }
}

Helloを使用する( = Helloに依存する)サービスクラスも用意しておきましょう。SpringBootではビジネスロジックをよくサービスクラスに書きますよね。

package jp.blackawa.examples;

/**
 * 挨拶を出力するサービスクラス.
 */
public class HelloService {
    private Hello hello;

    // Getter, Setterを用意しておく.
    public void setHello(Hello hello) {
        this.hello = hello;
    }

    public Hello getHello() {
        return hello;
    }
}

Hellointerfaceの実装は2つ用意されています(HelloBoys, HelloGirls)が、ここまでではHelloServiceがどちらに依存するかは決まっていません。 なんだかDIっぽくなってきましたね。そんな感じがしませんか?

3.Bean定義の用意

では、このプロジェクトがどっちのHellointerfaceの実装を使うのか、決めてあげましょう。 それにはxmlファイルを使います。懐かしいですね。 xmlファイルによるBean定義は、SpringBootが採用しているアノテーションJavaベースの設定よりも冗長で記述量も多く、エラーにも気づきにくいです。しかしその分xml定義の方が何が起こっているか明確に分かるはずです。

src/main/resources配下にbeans.xmlという名前でxmlファイルを用意してください。

<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">
</beans>

ここからが重要なので、段階的にいきましょう。上記の空ファイルができたら、次にBeanを登録しましょう。

Beanとはなんでしたっけ?SpringframeworkのDIコンテナで管理するオブジェクトでしたね。今回はHellointerfaceの2つの実装クラスをDIによって使い分けたいので、これらを登録すれば良さそうです。

       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="helloBoys"
+          class="jp.blackawa.examples.HelloBoys"/>
+    <bean id="helloGirls"
+          class="jp.blackawa.examples.HelloGirls"/>
</beans>

2つのBeanにIDとそれに紐づくclassが定義されているのが一目瞭然ですね。DIっぽくなってきましたね。

そして最後に、HelloServiceクラスが使用(依存)するのがどちらのHellointerfaceの実装クラスなのかを定義してあげましょう。

    <bean id="helloGirls"
          class="jp.blackawa.examples.HelloGirls"/>

+    <bean id="helloService"
+          class="jp.blackawa.examples.HelloService">
+        <property name="hello" ref="helloBoys" />
+    </bean>
</beans>

HelloServiceクラスをBeanとして登録しました。 そしてHelloServiceクラスの変数(property)であるhelloの参照を、HelloBoysに定義します。 なんと、あるクラスの変数が保持するインスタンスをBean定義のxmlファイルから定義してしまっています!IoC、制御の逆転というのがなんとなく感じられますね!

4.HelloServiceを使ってみよう

では動かしてみましょう。SpringBootを通して、このようなBeanが思い通りのオブジェクトを使って動いてくれるのは知っていると思いますが、こういうのは実際に動かしてこそ面白いものです。

mainメソッドからHelloServiceを呼び出して、変数として保持しているHellointerfaceの実装を取得してみましょう。

package jp.blackawa.examples;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main( String[] args ) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        HelloService service = (HelloService) context.getBean("helloService");
        Hello hw = service.getHello();
        hw.sayHello();
    }
}

読んで字の如く、

  1. クラスパス上のxmlファイルからApplicationContextを取得する。(ApplicationContextってなんでしたっけ?)
  2. そのApplicationContextからhelloServiceを取得する。
  3. HelloServiceの中に入っているHellointerfaceの実装クラスを取得し、文字列を出力させる

をしているように見えますね。そして実際にそうしています。

では…お持ちのIDEでこのmainメソッドを実行してみてください! コンソールにHello, Boys!と出力されますね。

おめでとうございます。これがDIです。

beans.xmlの定義を変えてみましょう。

    <bean id="helloService"
          class="jp.blackawa.examples.HelloService">
-        <property name="hello" ref="helloBoys" />
+        <property name="hello" ref="helloGirls" />
    </bean>
</beans>

Javaソースコードを一切いじらないままに出力内容がHello, Girls!に変わりますね。

おめでとうございます。

ちなみに、mainメソッド内で直接HelloServiceを呼ぶことも可能ですが、そうするとBean登録されていないサービスクラスを使うことになるので、getHelloから何も取得できません。だってどこでもその変数の初期化をしていませんからね。 普通ならHelloServiceインスタンス化する時に引数としてHellointerfaceの実装クラスを渡すはずなのに、おかしいですね。制御、逆転していますね。

肌で感じられましたか?

さて、ここまででSpringframeworkのうちDIだけを試してきましたが、肌で感じられましたか? これでSpringBootのうち@Autowired@Component@Serviceなどは怖くなくなりましたね! アノテーションをつけると、今日書いたような設定がうまいことやってもらえてControllerクラスから好きなServiceクラスが使用できるんですね。

次回はただの文字列を出力するのではなく、Webアプリケーションを作ってみましょう。まだxmlでの設定はまだ続きますよ。 今回もそのおかげで仕組みがよく分かりましたしね。

参考資料