215.5K Views
February 10, 22
スライド概要
動画はこちら→ https://www.youtube.com/watch?v=c5ha8FmdNmw
Spring Fest 2021の資料です。
DIコンテナなどSpringの基礎知識が既にある方を対象に、上級者へステップアップするために必要な発展的知識を解説します。
このセッションを聞けば、プロジェクトをリードできるエンジニアになれること間違いなし!かも!?
・DIコンテナの要点復習
・同じ型のBeanが複数あるとどうなる?
・Java Configクラスを分割・統合するには?
・環境によって設定値やBeanを変更するには?
・Beanはどうやって作られる?
Java、Spring、IntelliJ IDEA
プロになるための Spring上級知識 (株)クレディセゾン 多田真敏 2021年12月3日 Spring Fest 20211
このセッションについて ▸ Spring上級者に必須となる、DIコンテナ関連の 発展的な知識について解説します ▸ このセッションは【中級者向け】です ▸ SpringのDIコンテナなどについては、 ある程度知っていることを前提に解説します ▸ Spring Bootを利用していないことを前提とします ▸ 利用時の違いは必要に応じて解説します 2
このセッションの前提知識 ▸ Spring Fest 2020のこのセッション https://youtu.be/LGtdpsmMfvI 3
自己紹介 ▸ 多田真敏 (@suke̲masa) ▸ クレジットカード関連の システムを作ってる Javaエンジニア ▸ 元・研修トレーナー ▸ JSUGスタッフ 4
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 5
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 6
Springはコンテナを持っている ▸ コンテナ=インスタンスの入れ物 ※「インスタンス」とは、 もちろんJavaのインスタンスの ことです ▸ Bean=コンテナで管理されたインスタンス コンテナ Bean 必要に応じて 取り出して使う Bean Bean Bean Bean 7
Beanを定義する方法 ① コンポーネントスキャン ② Java Con g ③ 関数型Bean定義 (今回は割愛) ④ XML (今回は割愛) fi 8
Bean定義方法① コンポーネントスキャン ▸ コンポーネントスキャン =指定されたパッケージから@Componentが 付いたクラスを探す ▸ 見つけたらインスタンス化→コンテナに保存 9
Bean定義方法① コンポーネントスキャン ▸ 右記のアノテーションを クラスに付けると、 @Componentを付けた ことと同じになる ▸ クラスの役割に応じて 変える Java Con gも Bean! @Repository @Service @Controller @RestController @Configuration @ControllerAdvice @RestControllerAdvice ・・・ ※これらのアノテーションのソースコードを読むと、 @Componentが付いていることが分かります fi 10
Bean定義方法① コンポーネントスキャン ▸ Beanとしたいクラス package com.example; @Component public class Hoge { ... } @Componentが ▸ 設定クラス (Java Con g) 付いたクラスを 探すパッケージ @Configuration @ComponentScan(basePackages = "com.example") public class AppConfig {} fi 11
Bean定義方法② Java Con g ▸ Java Con gクラスにメソッドを作成し、 @Beanを付加する → メソッドの戻り値がBeanになる // @Componentは不要 public class Hoge { ... } // @ComponentScanは不要 @Configuration public class AppConfig { @Bean public Hoge hoge() { return new Hoge(); } } fi fi 12
Bean IDとは ▸ 各Beanを識別するためのID ▸ @ComponentでBean定義した場合、 「クラス名の頭文字を小文字にしたもの」 がBean ID ▸ @BeanでBean定義した場合、 メソッド名がBean ID 13
Bean IDとは Bean ID @Component public class FooBar { ... } fooBar @Component("foo_bar") public class FooBar { ... } foo_bar @Configuration public class FooConfig { @Bean public FooBar fooBar() { ... } fooBar } @Bean(name = "foo_bar") public FooBar fooBar2() { ... } foo_bar 14
コンテナを作る・Beanを取得する ▸ Java Con gクラスを指定して ApplicationContext (=コンテナ)を作成 ▸ コンポーネントスキャン・Java Con gどちらでも共通 ▸ getBean()でBeanを取得できる // コンテナの作成 ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // BeanをDIコンテナから取得(引数は欲しいBeanのデータ型) Hoge hoge = context.getBean(Hoge.class); fi fi 15
Spring Bootの場合 ApplicationContext context = SpringApplication.run(AppConfig.class, args); よくmain()メソッドに 書かれている処理ですね! 16
DI ▸ コンストラクタに@Autowiredを付加し、 引数に欲しいBeanを指定 ▸ コンテナが適切なBeanを引数に代入してくれる ▸ クラス内にコンストラクタが1つの場合、@Autowiredは省略可 コンテナ @Component public class Hoge { private final Fuga fuga; } @Autowired public Hoge(Fuga fuga) { this.fuga = fuga; } Hoge DI Fuga DI 17
いろんなDI ▸ コンストラクタインジェクション @Component public class Hoge { private final Fuga fuga; } @Autowired public Hoge(Fuga fuga) { this.fuga = fuga; } ▸ フィールドインジェクション @Component public class Hoge { } @Autowired Fuga fuga; ▸ メソッドインジェクション @Component public class Hoge { private Fuga fuga; } @Autowired public setFuga(Fuga fuga) { this.fuga = fuga; } 唯一Hogeを変更不能に できるので コンストラクタ インジェクション推奨 ※変更不能(イミュータブル)=フィールドの値を書換不可能 18
この章のまとめ ▸ コンテナ=インスタンス(Bean)の入れ物 ▸ コンポーネントスキャン ▸ @ComponentでBeanにするクラスを指定 ▸ @ComponentScanで探す範囲を指定 ▸ Java Con g ▸ @Beanを付加したメソッドの戻り値がBeanになる ▸ DI ▸ コンストラクタなどに@Autowiredを付加 fi 19
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 20
どうなる? @Component public class FooImpl1 implements Foo { ... } @Component public class FooImpl2 implements Foo { ... } @Component public class Bar { } 型Fooに合致するBeanが2つ →例外発生!! public Bar(Foo foo) { ... } 21
解決方法 ① @Qualifierを利用 ② @Primaryを利用 ③ DIする型を変える 22
① @Quali erを利用 @Component // Bean IDは"fooImpl1" public class FooImpl1 implements Foo { ... } @Component // Bean IDは"fooImpl2" public class FooImpl2 implements Foo { ... } @Component public class Bar { private final Foo foo; public Bar(@Qualifier("fooImpl1") Foo foo) { ... } 23 fi } DIしたいBeanのBean IDを指定
@Quali erのもう1つの使い方 ▸ @Qualifierを付加した独自アノテーションを 2つ作成 @Retention(RUNTIME) @Target({METHOD, TYPE, PARAMETER, FIELD}) @Qualifier public @interface Foo1 { ... } @Retention(RUNTIME) @Target({METHOD, TYPE, PARAMETER, FIELD}) @Qualifier public @interface Foo2 { ... } fi 24
@Quali erのもう1つの使い方 ▸ 独自アノテーションをBean定義箇所・DI箇所の 両方に付加 @Component @Foo1 public class FooImpl1 implements Foo { ... } @Component @Foo2 public class FooImpl2 implements Foo { ... } @Component public class Bar { public Bar(@Foo1 Foo foo) { ... } 25 fi } FooImpl1がDIされる
@Quali erの使い所 ▸ 将来的にDIするBeanの型が変わる可能性がある 場合や、型での区別が難しい場合に使う ▸ Bean IDを文字列でハードコードするよりも、 独自アノテーションを使う fi 26
② @Primaryを利用 @Component @Primary public class FooImpl1 implements Foo { ... } @Component public class FooImpl2 implements Foo { ... } @Component public class Bar { } public Bar(Foo foo) { ... } FooImpl1がDIされる (FooImpl2をDIしたい場合は @Qualifierを利用) 27
@Primaryの使い所 ▸ プロダクションコードではなるべく使わない ▸ @Qualifierなどで明示的に指定したほうが間違いが少ない ▸ 後で2つ目のBeanを追加することになった際、1つ目のBean が大量にあって修正箇所を少なくしたい場合は致し方なしかも ▸ テストコードでスタブをDIするために使う Barのテスト時に コレがDIされる @Configuration public class TestConfig { @Bean @Primary public Foo foo { return new StubFoo(); } } 28
③ DIする型を変える @Component public class FooImpl1 implements Foo { ... } @Component public class FooImpl2 implements Foo { ... } @Component public class Bar { } DIしたいBeanの型を明示 public Bar(FooImpl1 foo) { ... } 29
型の使い所 ▸ 可能な限りこの方法を使う ▸ アノテーションだと付け忘れのリスクが高い 30
この章のまとめ ▸ 同じ型のBeanがあったら ▸ @Qualifierを利用 ▸ @Primaryを利用 ▸ DIする型を変える ▸ 可能な限り型で判別。次点で@Qualifier。 @Primaryはテストのみ。 31
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 32
Java Con gの分割 ▸ 1つのJava Con gにすべてのBean定義を書くと クラスが肥大化する →Java Con gを分割する ▸ 例えば下記のように分割する ① Spring MVC用Con g ② ビジネスロジック+トランザクション用Con g ③ データソース+ORマッパー用Con g fi fi fi fi fi fi 33
分割したJava Con gをすべて使う方法 ① @Importでまとめる ② コンポーネントスキャン配下に置く ③ ApplicationContextのコンストラクタに すべて指定する fi 34
① @Importでまとめる Con g1にCon g2を合体 @Import(Config2.class) @Configuration public class Config1 { ... } @Configuration public class Config2 { ... } ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class); Con g1のみを指定 fi fi fi 35
[発展] @EnableXxxは@Import ▸ @EnableWebMvcなどのアノテーションは、 実は@Importで既存のJava Con gを 合体させている fi 36
② コンポーネントスキャン配下に置く @Configuration @ComponentScan(basePackages = "com.example") public class Config1 { ... } package com.example.foo; スキャン配下に置けば そのJava Con gは使われる @Configuration public class Config2 { ... } Con g1のみを指定 ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class); fi fi 37
③ ApplicationContextのコンストラクタに すべて指定する @Configuration public class Config1 { ... } @Configuration public class Config2 { ... } ApplicationContext context = new AnnotationConfigApplicationContext( Config1.class, Config2.class); 可変長引数で全Java Con gを指定 fi 38
使い所 ▸ 個人的にはほとんど 「②コンポーネントスキャン」で済ませる ▸ 特にSpring Bootの場合 ▸ 特定のJava Con gだけを使いたい場合は、 コンポーネントスキャン外に作って 「①@Import」を使う fi 39
この章のまとめ ▸ Java Con gは適切に分割する ▸ 分割したJava Con gをまとめる方法は ① @Import ② コンポーネントスキャン ③ ApplicationContextの引数 ▸ ほぼ②コンポーネントスキャンで済ませてOK fi fi 40
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 41
環境によってBeanを切り替えたい! ▸ 例:DataSourceをテスト環境と本番環境で切替 このままだとDataSourceの DI時に実行時例外 @Configuration public class AppConfig { @Bean public DataSource testDataSource() { return 組み込みデータベース; } } @Bean public DataSource productionDataSource() { return APサーバーから取得したデータソース; } 42
プロファイルを使う ▸ プロファイル=Beanのグループ ▸ @Profileで任意のプロファイル名を指定 ▸ 環境変数SPRING_PROFILES_ACTIVEで 実行時のプロファイルを指定 →指定したプロファイルのBeanが有効化される 43
プロファイルを使う @Configuration public class AppConfig { @Bean @Profile("test") public DataSource testDataSource() { ... } } @Bean @Profile("production") public DataSource productionDataSource() { ... } $ export SPRING_PROFILES_ACTIVE=test $ mvn compile exec:java -DmainClass=com.example.Main testDataSourceのみ 有効化される 44
プロファイルを使う ▸ Java Con gにも@Profileを付加できる @Configuration @Profile("test") 実行時にtestプロファイル 指定で有効化される public class TestConfig { @Bean public DataSource testDataSource() { ... } } @Configuration @Profile("production") 実行時にproductionプロファイル 指定で有効化される public class ProductionConfig { @Bean public DataSource productionDataSource() { ... } } fi 45
主な実行時プロファイル指定方法 優先順位 高 ① JUnitテスト 多用するとmvn testでの テスト実行時間が長くなるので注意 (DIコンテナが再作成されるため) ▸ @ActiveProfiles("test") ② JVMシステムプロパティ ▸ java -Dspring.profiles.active=test ... 低 ③ 環境変数 ▸ export SPRING_PROFILES_ACTIVE=test ※全ての方法で複数のプロファイル名を指定可能(カンマ区切り) 46
プロファイルの注意点 ▸ プロファイルに属していないBeanは 実行時にどのプロファイルを指定しても 有効化される このBeanはどんなプロファイルを 指定しても使われる @Configuration public class AppConfig { @Bean public Sample sample() { ... } } @Bean @Profile("test") public Sample testSample() { ... } このBeanはtestプロファイル 指定時のみ使われる 47
プロパティファイルから設定値を取得 この場合、クラスパス上の絶対パス ( le://やhttp://も利用可能) @Configuration @PropertySource("classpath:/sample.properties") public class AppConfig { ... } # src/main/resources/sample.properties sample.message=Hello! @Component public class Sample { } ${プロパティ名}で取得 public Sample( @Value("${sample.message}") String message) { ... } fi 48
@Valueで値を取得できる主な場所 優先順位 高 ① web.xmlのcontext-param ② JVMシステムプロパティ ▸ java -Dxxx=yyy ③ 環境変数 低 ④ プロパティファイル 49
プロファイルでプロパティファイルを切り替える
ファイル名にプレースホルダーを含める
@Configuration
@PropertySource(
"classpath:/sample-${SPRING_PROFILES_ACTIVE}.properties"
)
public class AppConfig { ... }
# src/main/resources/sample-test.properties
sample.message=Hello Test!
# src/main/resources/sample-production.properties
sample.message=Hello Production!
$ export SPRING_PROFILES_ACTIVE=test
$ mvn compile exec:java -DmainClass=com.example.Main
Hello Test!
Spring Bootでは必要の無いテクニック(後述)
50
Spring Bootの場合 ▸ 設定値はすべてapplication.propertiesに書く @PropertySource ▸ 特定のプロファイルでのみ利用する設定値は 不要 application-プロファイル名.propertiesに書く ▸ 例:application-test.properties application-production.properties ▸ application.propertiesの値は、 プロファイル指定の有無に関わらず必ず使われる ▸ application.propertiesと application-プロファイル名.propertiesに同名の設定があったら、 後者のものが使われる 51
Spring Bootの場合 ▸ Relaxed Binding ▸ application.propertiesのfoo.barを 環境変数のFOO_BARで上書き可能 52
Spring Bootの場合 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-con g より引用 低 ▸ プロパティ の優先順位 が決まって いる 高 優先順位 fi 53
この章のまとめ ▸ @ProfileでBeanをグループ化できる ▸ 実行時プロファイル名の指定方法はいろいろ ▸ @ActiveProfiles、-Dspring.profiles.active、 環境変数SPRING_PROFILES_ACTIVE ▸ プロパティファイルは@PropertySourceで指定 ▸ ファイル名にプレースホルダーを指定することで環境での切替可能 ▸ Spring Bootではapplication.propertiesに設定を書く ▸ Relaxed Bindingや設定の優先順位は覚えておく 54
目次 ① DIコンテナの要点復習 ② 同じ型のBeanが複数あるとどうなる? ③ Java Con gクラスを分割・統合するには? ④ 環境によってBeanや設定値を変更するには? ⑤ Beanはどうやって作られる? fi 55
Beanの生成プロセス ① Bean一覧表をつくる ② Beanのコンストラクタを実行する ③ Beanに後処理を行う 56
① Bean一覧表を作る ▸ 👇のような表を作る ▸ 実体は org.springframework.beans.factory.config.BeanDefinition クラス名 ID スコープ 遅延 依存先ID com.Foo foo singleton false (なし) com.Bar bar prototype true foo ・・・ ・・・ ・・・ ・・・ ・・・ ※この表はイメージです。正確なものではありません。 57
① Bean一覧表を作る ▸ 一覧表作成のために行うこと 1. コンポーネントスキャン 2. Java Con gクラス内の@Beanメソッド定義の確認 ▸ この時点では、 まだインスタンスは生成されない! fi 58
① Bean一覧表を作る インタフェース ▸ Bean一覧表作成後、BeanFactoryPostProcessorが 表の一部を無理やり書き換える ▸ 主な例 実装クラス ▸ PropertySourcesPlaceholderConfigurer ▸ @Valueの部分をプロパティから読んだものに置き換える ▸ LazyInitializationBeanFactoryPostProcessor ▸ Spring Bootの機能(spring.main.lazy-initialization=true) ▸ 全Beanの[遅延]をtrueにする 59
② Beanのコンストラクタを実行する ▸ Bean一覧表に基づいて、 コンストラクタでインスタンスを生成する ▸ この際、コンストラクタインジェクションも行われる ▸ インスタンス生成は、依存関係に基づいて 正しい順番で実行される ▸ 例:AがBに依存する→Bを先にインスタンス生成する ▸ フィールドインジェクションと メソッドインジェクションは、まだ行われない! 60
③ Beanに後処理を行う ▸ BeanPostProcessorがBeanインスタンスに後処理を施す ▸ 主な例 インタフェース ▸ AutowiredAnnotationBeanPostProcessor ▸ フィールドインジェクションとメソッドインジェクションを行う ▸ CommonAnnotationBeanPostProcessor 実装クラス ▸ @PostConstructが付加されたメソッドを実行する ▸ AbstractAutoProxyCreator ▸ AOPのためのプロキシインスタンスを作成する 61
補足 ▸ BeanFactoryPostProcessor・ BeanPostProcessorを自作することは稀 ▸ 学習目的ならアリ ▸ 生成プロセスを知っていれば、凝ったことを する際のトラブルを解決できるかも ▸ 例:Bean生成処理にはAOPをかけられない (まだプロキシが作られる前だから) 62
この章のまとめ ① Bean一覧表を作る ▸ 一覧表を作成後、BeanFactoryPostProcessorが一覧表の一部を 書き換える ② Beanのコンストラクタを実行する ▸ コンストラクタインジェクションも行われる ③ Beanに後処理を行う ▸ BeanPostProcessorがフィールドインジェクション・メソッドイン ジェクション・@PostConstructの実行・プロキシの作成などを行う 63
最後のまとめ ▸ 同じ型のBeanは@Qualifier・@Primary・ サブクラス型などで判別できる ▸ 分割したJava Con gは@Importや コンポーネントスキャンで統合する ▸ 設定値やBeanを環境ごとに使い分けるには プロファイルを利用する ▸ Beanの生成プロセスは ①一覧表作成→②コンストラクタ実行→③後処理 fi 64
更なる学習のために ▸ Spring徹底入門(翔泳社) ▸ https://www.shoeisha.co.jp/book/detail/ 9784798142470 ▸ Spring公式リファレンス(特にCoreの部分) ▸ [英語] https://docs.spring.io/spring-framework/ docs/current/reference/html/index.html ▸ [日本語] https://spring.pleiades.io/springframework/docs/current/reference/html/ 65
▸ このセッションが 未来のSpringプロフェッショナルの お役に立てれば嬉しいです! 66