SpringCore
DI
DI はソフトウェアデザインパターンの一つで「制御の反転の原則」を実現する。DI を利用するとクラスのインスタンス生成と依存関係の構築をアプリケーションのコードから分離することができる。
DI とは
結合度が高い実装
ユーザ登録を行うインターフェース
public interface UserService { void register(User user, String password); }
パスワードをハッシュ化するインターフェースの実装例
public interface PasswordEncoder { String encode(String password); }
ユーザ情報を操作するインターフェースの実装例
public interface UserRepository { User same(User user); int countByUsername(String username); }
ユーザ登録処理の実装例
public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserServiceImpl(javax.sql.DataSource dataSource) { this.userRepository = new JdbcUserRepository(datasource); this.passwordEncoder = new BCryptPasswordEncoder(); } public void register(User user, String password) { if (this.userRepository.countByUsername(user.getUsername()) > 0) { throw new UserAlredyRegistredException(); } user.setPassword(this.passwordEncoder.encode(rawPassword)); this.userRepository.save(user); } }
UserServiceImpl のコンストラクタで UserRepository と PasswordEncoder の実装クラスを生成しているため、部分的に実装を差し替えることは困難。この場合は以下のようにすることで結合度を低くすることができる。
「インターフェースにのみ依存する」ということを表していると考えられる。
public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public void register(User user, String password) { if (this.userRepository.countByUsername(user.getUsername()) > 0) { throw new UserAlredyRegistredException(); } user.setPassword(this.passwordEncoder.encode(rawPassword)); this.userRepository.save(user); } }
UserService クラスを使うアプリケーションの実装例
UserRepository userRepository = new JdbcUserRepository(datasource); PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); UserService userService = new UserServiceImpl(userRepository, passwordEncoder); // ...
しかしこの場合でも UserServiceImpl への各コンポーネントの設定は手動で行っており、変更が生じた場合のコストは大きい。ソースコードを直接修正しないといけないため。
あるクラスに必要となるコンポーネントを設定することを「依存性を注入(DI)する」もしくは「インジェクションする」という。依存性の注入(DI)を自動で行い、インスタンスを組み立ててくれる基盤のことを「DIコンテナ」という。
- DI コンテナによってアプリケーションで直接インスタンスを生成していたところを DI コンテナ経由でできる
- DI コンテナから取得するインスタンスで利用しているインスタンスも DI コンテナで管理され、設定状態で取得できる
DI を使うことで得られるメリット
DI コンテナでは ApplicationContext が DI コンテナの役割を担う。
ApplicationContext と Bean 定義
DI を使ってみる
以下の構成にしてみた。
Main.java
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Human human = context.getBean(Man.class); System.out.println(human.say()); } // I am man.
AppConfig.java
@Configuration
public class AppConfig {
@Bean
Man man() {
return new Man();
}
@Bean
Woman woman() {
return new Woman();
}
}
Man.java
public class Man implements Human {
public String say() {
return "I am man.";
}
}
Women.java
public class Woman implements Human {
public String say() {
return "I am woman.";
}
}
getBean のルックアップによって human.say()
の内容が切り替わることがわかった。
Bean の生成方法
Configuration 方法
アノテーションベース configuration の場合は @Component
と @ComponentScan("x.y.z")
のアノテーションを組み合わせることで Bean 定義を Bean 定義ファイルに定義するのではなく、DI したいクラスをスキャン対象にして DI コンテナに登録することができる。
インジェクションの種類
- セッターインジェクション
- コンストラクタインジェクション
- フィールドインジェクション
- インジェクションしたいフィールドに
@Autowired
を付与する
- インジェクションしたいフィールドに
オートワイヤリング
@Bean
メソッドや
コンポーネントスキャン
@ConponentScan
または <context:component-scan>
要素を使用する。スキャン対象のパッケージをクラスローダーからスキャンするため、範囲が広いほうが処理が遅くなる。起動処理の時間の遅さにつながる。
スキャン対象のアノテーションは以下
アノテーション | 説明 |
---|---|
@Controller | MVC パターンの C の役割を担うコンポーネントであることを示すアノテーション。 |
@Service | ビジネスロジックを提供するコンポーネントであることを示すアノテーション。 |
@Repository | データの永続化に関わる処理を提供するコンポーネントであることを示すアノテーション。ORM などの永続化ライブラリを利用してデータの CRUD 処理を実装する |
@Component | 上記に当てはまらないコンポーネント(ユーティリティクラスやサポートクラスなど) |
コンポーネントスキャンではフィルターが可能
Bean のスコープ
DI コンテナを利用するメリットとして、Bean のスコープの管理をコンテナに任せることができる。コンテナに管理される Bean はデフォルトでシングルトンである。以下のコードは UserService
クラスのシングルトンインスタンスを取得する。
UserService userService = context.getBean(UserService.class)
Spring Framework で利用可能なスコープ
スコープ | 説明 |
---|---|
singleton | DIコンテナ起動時にBeanインスタンスを生成し、同一のインスタンスを共有して利用する。デフォルトの設定 |
prototype | Bean取得時に毎回インスタンスを生成する。スレッドあんセーフなBeanの場合は、singletonスコープを利用できないためprototypeを利用する |
session | HTTPのセッション単位でBeanインスタンスを生成する |
request | HTTPのリクエスト単位でBeanインスタンスを生成する |
globalSession | 省略 |
application | サーブレットのコンテキスト単位でBeanインスタンスを生成する |
カスタムスコープ | 独自定義 |
以下の場合は別インスタンスになる。
AppConfig.java
@Bean @Scope(prototype) Man man() { return new Man(); }
Human human1 = context.getBean(Man.class); Human human2 = context.getBean(Man.class);
異なるスコープのインジェクション
- ルックアップメソッドインジェクション
- Scope Proxy
Bean のライフサイクル
DI コンテナで管理されている Bean のライフサイクルは次の 3 つのフェーズで構成される
- 初期化フェーズ
- 利用フェーズ
- 終了フェーズ
ほとんどの時間は利用フェーズ。
Configuration の分割
DI コンテナで管理する Bean が多くなると Configuration も肥大化していく。 Configuration の範囲を明確にして、可読性を向上させるには必要に応じて Configuration の分割を行う。
Configuration のプロファイル化
環境ごとに Configuration をグループ化できる。
AOP
複数のモジュールにまたがって存在する処理=横断的関心事。具体例として以下のようなものが存在する。
- セキュリティ
- ログ出力
- トランザクション
- モニタリング
- キャッシュ
- 例外ハンドリング
プログラムから横断的関心事を取り除き、一箇所に集めることを「横断的関心事の分離」と呼び、これを実現する手法をアスペクト指向プログラミングをいう。
AOP の概要
AOP とはアスペクト指向プログラミングの略で、複数クラスに点在する横断的関心事を中心に設計や実装を行うプログラミング手法。AOP を利用するとインスタンスに外部から共通的な機能を入れ込むことができるようになる。つまりアプリケーションのコードから共通的な機能を分離することができる。
AOP のコンセプト
用語 | 説明 |
---|---|
Aspect | AOPの単位となる横断的な関心事を示すモジュール。例:「ログを出力する」「例外をハンドリングする」など |
Join Point | 横断的関心事を実行するポイントのこと。例:メソッド実行時、例外スロー時。SpringのAOPではJoin Pointはメソッドの実行時 |
Advice | 特定のJoin Pointで実行されるコード |
Pointcut | 実行対象のJoin Pointを選択する表現。Join Pointのグループと捉えることもできる |
Weaving | アプリケーションコードの適切なポイントにAspectを入れ込む処理のこと |
Target | AOP処理によって処理フローが変更されたオブジェクトのこと |
Spring AOP
実際にやってみる
ComponentScan 対象の任意のクラスの任意のメソッドで開始のログを出力できるようにします。
@Aspect @Component public class MethodStartLoggingAspect { @Before("execution(* *..*.*(..))") public void startLog(JoinPoint jp) { System.out.println("メソッド開始:" + jp.getSignature()); } @After("execution(* *..*.*(..))") public void afterLog(JoinPoint jp) { System.out.println("メソッド終了:" + jp.getSignature()); } }
ここで @EnableAspectJAutoProxy(proxyTargetClass = true)
のかわりに @EnableAspectJAutoProxy
を指定すると動作しない。CGLibのプロキシの仕組みを利用してプロキシ生成すると良いが、この違いは分かっていない。
@EnableAspectJAutoProxy(proxyTargetClass = true) @Configuration @ComponentScan("chap2.section2.example1") public class AppConfig { }
@SuppressWarnings("resource") public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Human human = context.getBean(Man.class); human.say(); human = context.getBean(Woman.class); human.say(); } // メソッド開始:void chap2.section2.example1.Man.say() // I am man. // メソッド終了:void chap2.section2.example1.Man.say() // メソッド開始:void chap2.section2.example1.Woman.say() // I am woman. // メソッド終了:void chap2.section2.example1.Woman.say()
Spring で利用可能な Advice は以下の 5 つがある
Advice | 概要 |
---|---|
Before | JoinPointの前に実行されるAdvice |
After Returning | Join Pointが正常終了した後に実行されるAdvice |
After Throwing | Join Pointで例外がスローされた後に実行されるAdvice |
After | Join Pointの後に実行されるAdvice。Join Pointの正常終了や例外のスローにかかわらず、常に実行される |
Around | Join Pointの前後で実行されるAdvice。対象のメソッドの実行も行う |
Pointcut 式
Pointcut の式は AspectJ の様々な式を用いて Join Point を選択できる。Pointcut はマッチングされるパターンごとに指示子が異なる。
execution 指示子
execution 指示子は以下の書式で記述する。
- Pointcut 式で利用可能なワイルドカード
ワイルドカード | 説明 |
---|---|
* | 基本的には任意の文字列を表すが、パッケージを表現する場合は任意のパッケージ1階層を表す。メソッドの引数を表現する場合は、1つの数を表す |
.. | パッケージを表現する場合は任意の0以上のパッケージを表す。メソッドの引数を表現する場合は任意の0個以上の引数を表す |
+ | クラス名の後に指定することにより、そのクラスとそのサブクラス/実装クラスすべてを表す |
指示子いろいろ
Advice の対象オブジェクトや引数も取得できる
型で対象の Join Point を指定する方法の例は以下
within
を用いる。以下の例はメソッド開始時は Human インターフェースの実装クラスのメソッドすべてを対象にしている。メソッド終了時は Human クラスのみを対象にしている(ので Aspect されない)。
AppConfig.java
@Aspect @Component public class MethodLoggingAspect { @Before("within(chap2.section2.example2.Human+)") public void startLog(JoinPoint jp) { System.out.println("メソッド開始:" + jp.getSignature()); } @After("within(chap2.section2.example2.Human)") public void afterLog(JoinPoint jp) { System.out.println("メソッド終了:" + jp.getSignature()); } } // メソッド開始:void chap2.section2.example2.Man.say() // I am man. // メソッド開始:void chap2.section2.example2.Woman.say() // I am woman.
AOJ が使われている例
- トランザクション管理
- 認可処理
- キャッシュ処理
- 非同期処理
- 対象のメソッドに
@Async
を付与し、戻り値としてjava.util.concurrent.CompletableFuture
やorg.springframework.web.context.request.async.DefferedResult
を返すようにすればその処理が別スレッドで実行される
- 対象のメソッドに
- リトライ処理
データバインディング
外部から指定された入力値を Java オブジェクトのプロパティに設定する処理のこと
@Component
でプロパティクラスを DI コンテナに登録することで使えるようにできる。以下の例は、Bean 定義でバインドする実装例である。
@Component public class Sample { @Value("${sampleName:hoge}") String sampleName; public void say() { System.out.println(sampleName); } }
@SuppressWarnings("resource") public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Sample s = context.getBean(Sample.class); s.say(); } // hoge