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.beans
とorg.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
メソッドを定義するHello
interfaceと、それを実装して文字列を標準出力するクラスを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; } }
Hello
interfaceの実装は2つ用意されています(HelloBoys
, HelloGirls
)が、ここまでではHelloService
がどちらに依存するかは決まっていません。
なんだかDIっぽくなってきましたね。そんな感じがしませんか?
3.Bean定義の用意
では、このプロジェクトがどっちのHello
interfaceの実装を使うのか、決めてあげましょう。
それには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コンテナで管理するオブジェクトでしたね。今回はHello
interfaceの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
クラスが使用(依存)するのがどちらのHello
interfaceの実装クラスなのかを定義してあげましょう。
<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
を呼び出して、変数として保持しているHello
interfaceの実装を取得してみましょう。
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(); } }
読んで字の如く、
- クラスパス上のxmlファイルから
ApplicationContext
を取得する。(ApplicationContextってなんでしたっけ?) - その
ApplicationContext
からhelloService
を取得する。 HelloService
の中に入っているHello
interfaceの実装クラスを取得し、文字列を出力させる
をしているように見えますね。そして実際にそうしています。
では…お持ちの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
をインスタンス化する時に引数としてHello
interfaceの実装クラスを渡すはずなのに、おかしいですね。制御、逆転していますね。
肌で感じられましたか?
さて、ここまででSpringframeworkのうちDIだけを試してきましたが、肌で感じられましたか?
これでSpringBootのうち@Autowired
、@Component
、@Service
などは怖くなくなりましたね!
アノテーションをつけると、今日書いたような設定がうまいことやってもらえてControllerクラスから好きなServiceクラスが使用できるんですね。
次回はただの文字列を出力するのではなく、Webアプリケーションを作ってみましょう。まだxmlでの設定はまだ続きますよ。 今回もそのおかげで仕組みがよく分かりましたしね。
参考資料
- Core Container - 公式ドキュメントのcore部分に関する記述
- The IoC Container - 公式ドキュメントのIoCコンテナに関する記述
- Inversion of Control コンテナと Dependency Injection パターン - IoCコンテナとは何かについての日本語記事
- Spring Tutorial for Beginners − o7planning - Springframeworkの入門ブログ記事(英語). サンプルコードはだいたいここで学ばせてもらいました。