165.6K Views
January 10, 22
スライド概要
動画はこちら→ https://www.youtube.com/watch?v=LGtdpsmMfvI
#jsug Spring Fest 2020での発表資料です。
Springの根幹機能はDIとAOPです。しかし、普通に使っているだけだと「@Autowiredを付ければ代入されるんでしょ?」「@Transactionalを付ければ割り込みされるんでしょ?」くらいの曖昧な理解に陥りがちです。
このセッションでは、DIやAOPの重要機能や仕組みを丁寧に解説します。Springの根幹を理解して、初級者から中級者へ、中級者から上級者へステップアップしましょう!
※発表では無かった内容を[おまけ]として加えました
Java、Spring、IntelliJ IDEA
#jsug 今こそ知りたい Spring DI AOP (株)カサレアル 多田真敏 2020年12月17日 JSUG Spring Fest1 (C) CASAREAL, Inc. All rights reserved.
#jsug このセッションについて ▸ Springの基礎中の基礎である「コンテナ」「DI」 「AOP」について、分かりやすく解説します ▸ 基本的に初心者向けですが、Springを利用している 全ての方が対象です ▸ Spring Framework 5.3.1 / Spring Boot 2.4.0 で検証しています ▸ バージョンが異なる場合、内容も異なる可能性があります 2 (C) CASAREAL, Inc. All rights reserved.
#jsug 自己紹介 ▸ 多田真敏(@suke̲masa) ▸ 研修トレーナー@カサレアル ▸ Java / Spring / Golang / Python / Kubernetes ▸ VMware認定講師 ▸ 日本Springユーザ会スタッフ 3 (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 4 (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 5 (C) CASAREAL, Inc. All rights reserved.
#jsug Springはコンテナを持っている ▸ コンテナ=インスタンスの入れ物 ※「インスタンス」とは、 もちろんJavaのインスタンスの ことです ▸ Bean=コンテナで管理されたインスタンス コンテナ Bean 必要に応じて 取り出して使う Bean Bean Bean Bean 6 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナは全Springプロダクトのベース ▸ 例:Spring MVCはDispatcherServlet内に コンテナを持っている DispatcherServlet コンテナ 必要に応じて View Resolver Handler Mapping ・・・ 取り出して使う 7 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナの機能(詳細は後述) ① Dependency Injection ② スコープの管理 ③ プロキシの作成 8 (C) CASAREAL, Inc. All rights reserved.
#jsug Beanを定義する方法 ① コンポーネントスキャン ② Java Con g ③ 関数型Bean定義 (今回は割愛) ④ XML (今回は割愛) これらの方法は、全て併用可能! 9 fi (C) CASAREAL, Inc. All rights reserved.
#jsug Bean定義方法① コンポーネントスキャン ▸ コンポーネントスキャン =指定されたパッケージから@Componentが 付いたクラスを探す ▸ 見つけたらインスタンス化→コンテナに保存 10 (C) CASAREAL, Inc. All rights reserved.
#jsug Bean定義方法① コンポーネントスキャン ▸ 右記のアノテーションを クラスに付けると、 @Componentを付けた ことと同じになる @Repository @Service @Controller @RestController @Configuration ▸ クラスの役割に応じて 変える @ControllerAdvice @RestControllerAdvice ・・・ ※これらのアノテーションのソースコードを読むと、 @Componentが付いていることが分かります (C) CASAREAL, Inc. All rights reserved. 11
#jsug Bean定義方法① コンポーネントスキャン ▸ Beanとしたいクラス package com.example; @Component public class Hoge { ... } @Componentが ▸ 設定クラス (Java Con g) 付いたクラスを 探すパッケージ @Configuration @ComponentScan(basePackages = "com.example") public class AppConfig {} 12 fi (C) CASAREAL, Inc. All rights reserved.
#jsug Bean定義方法① コンポーネントスキャン ▸ コンポーネントスキャン範囲は、basePackagesで 指定したパッケージのサブパッケージ以下も含む @Configuration @ComponentScan( basePackages = "com.example") public class AppConfig {} com スキャン範囲 example foo bar baz 13 (C) CASAREAL, Inc. All rights reserved.
#jsug Bean定義方法① コンポーネントスキャン ▸ basePackagesを指定しなかった場合、 @ComponentScanが付いたクラスのパッケージ がbasePackagesとなる package com.example.config; @Configuration @ComponentScan // basePackages無し public class AppConfig {} これが basePackages になる! 14 (C) CASAREAL, Inc. All rights reserved.
#jsug 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(); } } 15 fi fi (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナを作る・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); 16 fi fi (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナ作成で行われること ▸ コンポーネントスキャンの場合 1. @Componentが付いたクラスを探す 2. 見つけたらインスタンス化 3. コンテナに保存 コンテナ作成は 重い処理なので、 1度コンテナを 作成したら それを使い回す! ▸ Java Con gの場合 ※実際には、インスタンス生成のタイミングは 1. @Beanメソッドを実行 スコープ(後述)によって異なります。 ここで説明しているのはsingletonスコープの場合です 2. 戻り値をコンテナに保存 17 fi (C) CASAREAL, Inc. All rights reserved.
#jsug
Spring Bootの場合
これがbasePackages
@Configuration
@ComponentScan
package com.example;
@EnableAutoConfiguration
を組み合わせたアノテーション
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
// コンテナの作成
ApplicationContext context =
SpringApplication.run(MyApplication.class);
}
}
※@EnableAutoConfigurationについては下記資料を参照
https://www.slideshare.net/masatoshitada7/spring-boot-jjug
18
(C) CASAREAL, Inc. All rights reserved.
#jsug [おまけ] Bean IDとは ▸ 各Beanを識別するためのID ▸ Bean取得などに使う(近年は滅多に使わない) Hoge hoge = (Hoge) context.getBean("Bean ID"); ▸ @ComponentでBean定義した場合、 「クラス名の頭文字を小文字にしたもの」がBean ID ▸ @BeanでBean定義した場合、メソッド名がBean ID 19 (C) CASAREAL, Inc. All rights reserved.
#jsug [おまけ] 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 20 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナのまとめ ▸ コンテナ=インスタンス(Bean)の入れ物 ▸ コンポーネントスキャン ▸ @ComponentでBeanにするクラスを指定、 @ComponentScanで探す範囲を指定 ▸ Java Con g ▸ @Beanを付加したメソッドの戻り値がBeanになる 21 fi (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 22 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナの機能(再掲) ① Dependency Injection ② スコープの管理 ③ プロキシの作成 23 (C) CASAREAL, Inc. All rights reserved.
#jsug Dependency Injectionとは ▸ 日本語で言うと 依存性の注入 24 (C) CASAREAL, Inc. All rights reserved.
#jsug 25 (C) CASAREAL, Inc. All rights reserved.
#jsug Dependency Injectionとは 依存性の注入 必要なインスタンス (自動的)代入 26 (C) CASAREAL, Inc. All rights reserved.
#jsug newしないで代入してもらう=DI ▸ DIは、Bean生成時にコンテナが行う ▸ newしないことで、単体テストなどの時に インスタンスの差し替えが可能 ❌ objA new ⭕ objA 代入 objB objB 27 (C) CASAREAL, Inc. All rights reserved.
#jsug アノテーション+コンポーネントスキャンの場合 ▸ コンストラクタに@Autowiredを付加し、 引数に欲しいBeanを指定 ▸ コンテナが適切なBeanを引数に代入してくれる コンテナ @Component public class Hoge { private final Fuga fuga; } @Autowired public Hoge(Fuga fuga) { this.fuga = fuga; } DI Fuga DI ※クラス内にコンストラクタが1つのみの場合、 @Autowiredは省略可能 Hoge (C) CASAREAL, Inc. All rights reserved. 28
#jsug @Autowiredの省略 ▸ クラス内にコンストラクタが1つのみの場合、 @Autowiredを省略可能 @Component public class Hoge { private final Fuga fuga; @Autowired 省略可能 } public Hoge(Fuga fuga) { this.fuga = fuga; } 29 (C) CASAREAL, Inc. All rights reserved.
#jsug Java Con gの場合 ▸ @Beanメソッドの引数に欲しいBeanを指定し、 メソッド内に代入するコードを記述 ▸ コンテナが適切なBeanを引数に代入してくれる コンテナ @Configuration public class AppConfig { Hoge DI Fuga DI 30 (C) CASAREAL, Inc. All rights reserved. fi } @Bean public Hoge hoge(Fuga fuga) { return new Hoge(fuga); }
#jsug いろんなDI ※メソッドインジェクションとも呼びます ▸ コンストラクタインジェクション @Component public class Hoge { private final Fuga fuga; } @Autowired public Hoge(Fuga fuga) { this.fuga = fuga; } ▸ フィールドインジェクション @Component public class Hoge { } @Autowired Fuga fuga; ▸ setterインジェクション @Component public class Hoge { private Fuga fuga; } @Autowired public setFuga(Fuga fuga) { this.fuga = fuga; } 唯一Hogeを変更不能に できるので コンストラクタ インジェクション推奨 ※変更不能(イミュータブル)=フィールドの値を書換不可能 (C) CASAREAL, Inc. All rights reserved. 31
#jsug DIのまとめ ▸ DI=必要なインスタンスの代入 ▸ DIはコンテナがBeanを生成する際に行う ▸ コンストラクタに引数を付けて@Autowired or @Beanメソッドに引数 でBeanを代入可能 ▸ コンストラクタインジェクションが推奨 32 (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 33 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナの機能(再掲) ① Dependency Injection ② スコープの管理 ③ プロキシの作成 34 (C) CASAREAL, Inc. All rights reserved.
#jsug スコープとは ▸ コンテナが管理している、Beanの有効範囲 ▸ Beanがいつ生成され、いつ破棄されるか スコープの種類 説明 singleton インスタンスは1つのみ(デフォルト) prototype 必要な時にインスタンスが毎回作られる request リクエストと同じ session セッションと同じ 35 (C) CASAREAL, Inc. All rights reserved.
#jsug デフォルトはsingleton ▸ 1つのインスタンスが 各所で使い回される ▸ フィールドで値を 保持するのは厳禁!! A B 値を 保存 値が 見える Bean ▸ セキュリティ事故に… 値 36 (C) CASAREAL, Inc. All rights reserved.
#jsug スコープの考え方 ▸ ほとんどのBeanはsingletonでOK ▸ それ以外にしたくなったら、まず設計を見直す ▸ たまにsession使うかな・・・?くらい。 37 (C) CASAREAL, Inc. All rights reserved.
#jsug スコープの指定方法① ▸ @Scope("スコープ名")をBeanに付加 @Scope("session") @Component public class Hoge { ... } @Configuration public class AppConfig { } @Scope("request") @Bean public Hoge hoge() { ... } 38 (C) CASAREAL, Inc. All rights reserved.
#jsug スコープの指定方法② ▸ @RequestScope・@SessionScopeも用意されている @SessionScope @Component public class Hoge { ... } @Configuration public class AppConfig { } @RequestScope @Bean public Hoge hoge() { ... } 39 (C) CASAREAL, Inc. All rights reserved.
#jsug スコープのまとめ ▸ singleton・prototype・request・sessionの 4種類 ▸ デフォルトはsingleton。値の保持厳禁! ▸ @Scopeで指定 40 (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 41 (C) CASAREAL, Inc. All rights reserved.
#jsug コンテナの機能(再掲) ① Dependency Injection ② スコープの管理 ③ プロキシの作成 42 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシとは ▸ 本来Beanとなるはずだった インスタンスを ラップしたBean <<interface>> Hoge プロキシ ▸ すべてのBeanにプロキシが 付くわけではない(後述) HogeImpl ▸ ①インタフェース または ②継承 を利用して作られる 43 (C) CASAREAL, Inc. All rights reserved.
#jsug 44 (C) CASAREAL, Inc. All rights reserved.
#jsug どういうこと? これがBeanになるの ではなく・・・ @Component public class HogeImpl implements Hoge { public void doSomething() { ... } } こんなクラスが 実行時に生成され、 これがBeanになる! // このコードはイメージです public class HogeProxy implements Hoge { @Override public void doSomething() { ... } } 45 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの目的① AOP ▸ 本来の処理の前後 に、割り込み処理 を行う // このコードはイメージです public class HogeProxy implements Hoge { public void doSomething() { // 本来のインスタンス Hoge hoge = ...; // 前処理の割り込み ▸ トランザクションの 開始・終了、 権限チェックなど ▸ 詳細は後述 interceptor.doBefore(); // 本来の処理 hoge.doSomething(); // 後処理の割り込み } } interceptor.doAfter(); 46 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの目的② スコープの違いを吸収 @Scope("singleton") @Component public class SingletonScopeBeanImpl implements SingletonScopeBean { private final RequestScopeBean rsb; @Autowired public SingletonScopeBean(RequestScopeBean rsb) { this.rsb = rsb; } こちらもsingletonに } public void doSomething() { rsb.execute(); } なってしまう? → DIされるのは プロキシ 47 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの目的② スコープの違いを吸収 // このコードはイメージです public class RequestScopeBeanProxy implements RequestScopeBean { ApplicationContext context; } 毎回コンテナから 取得している! @Override public void execute() { RequestScopeBean rsb = context.getBean(RequestScopeBean.class); rsb.execute(); } ※このプロキシはインタフェースで作成していますが、 継承の場合でも同様です (C) CASAREAL, Inc. All rights reserved. 48
#jsug プロキシの目的③ Beanをシングルトンに保つ @Configuration Sample1のインスタンスが public class HogeConfig { Sample1のインスタンスが 2つ作られる!? @Bean 2つ作られる!? public Sample1 sample1() { シングルトンでなくなる!? return new Sample1(); } @Bean public Sample2 sample2() { return new Sample2(sample1()); } @Bean public Sample3 sample3() { return new Sample3(sample1()); } } 49 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの目的③ Beanをシングルトンに保つ // このコードはイメージです public class HogeConfigProxy extends HogeConfig { @Override public Sample1 sample1() { if (既にDIコンテナにSample1のBeanがあったら) { return context.getBean(Sample1.class) } else { return new Sample1(); } sample1()が何回実行されても } } ... Beanはシングルトン! 50 (C) CASAREAL, Inc. All rights reserved.
#jsug どんなBeanにプロキシが作られるか ① AOPを利用しているBean TARGET̲CLASS → CGLIB INTERFACES → JDK ② @ScopeでproxyMode=TARGET_CLASS またはINTERFACESが指定されているBean ③ @ComponentScanにscopedProxy=TARGET_CLASS またはINTERFACESが指定されているBean ④ @ConfigurationにproxyBeanMethods=true が指定されているJava Con g(デフォルトでtrue) 51 fi (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの作られ方① JDK Proxy <<interface>> Hoge ▸ デフォルトの挙動 ▸ 本来のクラスが実装している全ての 実装 インタフェースを実装したクラスが プロキシ 実行時に作られ、 それがプロキシ(かつBean)となる HogeImpl ▸ java.lang.reflect.Proxyの機能 ▸ 何らかのインタフェースを実装した クラスに使われる 52 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの作られ方② CGLIB Poxy HogeImpl ▸ 本来のクラスのサブクラスが 実行時に作られ、 継承 それがプロキシ(かつBean)と プロキシ なる ▸ Springが内包している ライブラリ「CGLIB」の機能 HogeImpl ▸ 何のインタフェースも 実装してないクラスに使われる 53 (C) CASAREAL, Inc. All rights reserved.
#jsug Beanがインタフェースを実装していても CGLIB Proxyを使いたい場合 ① Spring BootではデフォルトがCGLIB Proxy ▸ spring.aop.proxy-target-class=trueであるため (falseにすればJDK Proxy) ② @Scope(proxyMode=TARGET_CLASS) ▸ 付加したBeanのみが対象 ③ @ComponentScan(scopedProxy=TARGET_CLASS) ▸ コンポーネントスキャンした全Beanが対象 54 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシの制約 ▸ JDK Proxy ▸ 少なくとも1つのインタフェースを実装しなければ ならない ▸ CGLIB Proxy ▸ クラスやメソッドを nalにしてはならない 55 fi (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシのまとめ ▸ プロキシ=本来Beanとなるはずだったインスタンスを ラップしたBean ▸ インタフェース(JDK Proxy)または 継承(CGLIB Proxy)を利用して作られる ▸ Spring BootではデフォルトがCGLIB Proxy ▸ AOP、スコープの違いの吸収、Beanをシングルトンに保つ、 が目的 ▸ 各プロキシの制約に注意 56 (C) CASAREAL, Inc. All rights reserved.
#jsug 目次 ① Springのコンテナとは ② DIとは ③ スコープとは ④ プロキシとは ⑤ AOPとは 57 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPとは ▸ 本来の処理の前後に、割り込み処理を行う ▸ トランザクションの開始・終了、権限チェックなど 58 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPの使い方 @Configuration @EnableAspectJAutoProxy public class AspectConfig { ... } Spring Bootでは不要 59 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPの使い方 com.exampleパッケージの 全クラス・全メソッドの前後に割り込む @Aspect @Component public class LoggingAspect { @Around("execution(* com.example.*.*(..))") public Object log(ProceedingJoinPoint jp) throws Exception { System.out.println("start"); // 前処理 Object value = jp.proceed(); System.out.println("end"); } } // 本来の処理 // 後処理 return value; 60 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPの使い方 package com.example; 割り込み対象は 必ずBean @Component public class HelloService { public void hello() { System.out.println("hello"); } } 実行結果 start hello end 61 (C) CASAREAL, Inc. All rights reserved.
#jsug 割り込める場所 ▸ メソッド開始直前 → @Before ▸ メソッド終了直後 → @AfterReturning・@AfterThrowing ・@After ▸ メソッド開始直前・終了直後の両方 → @Around 62 (C) CASAREAL, Inc. All rights reserved.
#jsug ポイントカット式 ▸ どのクラス・どのメソッドに割り込むかの 条件を指定する execution(* com.example.*.*(..)) 戻り値の 型は任意 com.exampleパッケージの 全クラス・全メソッド 引数の個数は 任意 execution(@annotation(com.example.MyAnno)) @com.example.MyAnnoアノテーションが 付加された全メソッド ポイントカットの正式な文法は、AspectJのリファレンスを参照 https://www.eclipse.org/aspectj/doc/next/progguide/language-joinPoints.html (C) CASAREAL, Inc. All rights reserved. 63
#jsug プロキシによるAOPの実現 ▸ プロキシ内に、割り込み処理を実行する Interceptorが同居する HelloService プロキシ Interceptor HelloService Impl 64 (C) CASAREAL, Inc. All rights reserved.
#jsug プロキシによるAOPの実現 // このコードはイメージです public class HelloServiceProxy implements HelloService { @Override public void hello() { HelloService target = ...; // 割り込み対象 Interceptor interceptor = ...; interceptor.doBefore(); target.hello(); // 前処理 // 本来の処理 interceptor.doAfter(); } // 割り込み処理担当 // 後処理 } 65 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPの注意点 ▸ 同一クラス内のメソッド呼び出しには割り込みがされない! ▸ 特に@Transactionalで間違い多し @Component public class SampleService { public void method1() { System.out.println("method1()"); method2(); このmethod2()には } 割り込みされない! } public void method2() { System.out.println("method2()"); } 66 (C) CASAREAL, Inc. All rights reserved.
#jsug なぜ割り込みされないのか 同一クラス内だから Interceptorが実行されない! 67 (C) CASAREAL, Inc. All rights reserved.
#jsug 割り込みされるようにするには Spring Bootでも付ける @Configuration @EnableAspectJAutoProxy(exposeProxy = true) public class AspectConfig { ... } @Component public class SampleService { public void method1() { ... ((SampleService) AopContext.currentProxy()) .method2(); } public void method2() { ... } } 68 (C) CASAREAL, Inc. All rights reserved.
#jsug AOPのまとめ ▸ @Aroundなどのアノテーションと、 ポイントカット式で割り込み対象を指定する ▸ AOPはプロキシによって実現されている ▸ 同一クラス内のメソッド呼び出しには 割り込みされないので注意 69 (C) CASAREAL, Inc. All rights reserved.
#jsug まとめ 70 (C) CASAREAL, Inc. All rights reserved.
#jsug 本日のまとめ ▸ SpringはコンテナでBeanを管理している ▸ DIとは、必要なBeanの自動的代入 ▸ スコープは4種類。デフォルトはsingleton ▸ Beanにはプロキシが作られている場合がある ▸ AOPはプロキシによって実現されている 71 (C) CASAREAL, Inc. All rights reserved.
#jsug おすすめ市販書籍 ▸ 上記以外は、内容の信頼性が微妙なので、 個人的にはおすすめしません 72 (C) CASAREAL, Inc. All rights reserved.
#jsug ご清聴ありがとうございました! 73 (C) CASAREAL, Inc. All rights reserved.