技術メモ

技術メモ

ラフなメモ

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コンテナ」という。

f:id:tutuz:20190330155431p:plain

  • DI コンテナによってアプリケーションで直接インスタンスを生成していたところを DI コンテナ経由でできる
  • DI コンテナから取得するインスタンスで利用しているインスタンスも DI コンテナで管理され、設定状態で取得できる

DI を使うことで得られるメリット

インスタンスのスコープを制御できる
インスタンスのライフサイクルを制御できる
・共通機能を組み込める
コンポーネント間が疎結合になるため、単体テストがしやすくなる

DI コンテナでは ApplicationContext が DI コンテナの役割を担う。

ApplicationContext と Bean 定義

DI を使ってみる

以下の構成にしてみた。

f:id:tutuz:20190330212122p:plain

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 メソッドや 要素で明示的に Bean 登録を定義しなくても自動で DI コンテナにインジェクションさせる仕組み。オートワイヤリングには「型による解決」と「名前による解決」がある。

コンポーネントスキャン

@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 指示子は以下の書式で記述する。

execution(戻り値 パッケージ.クラス.メソッド(引数))
ワイルドカード 説明
* 基本的には任意の文字列を表すが、パッケージを表現する場合は任意のパッケージ1階層を表す。メソッドの引数を表現する場合は、1つの数を表す
.. パッケージを表現する場合は任意の0以上のパッケージを表す。メソッドの引数を表現する場合は任意の0個以上の引数を表す
+ クラス名の後に指定することにより、そのクラスとそのサブクラス/実装クラスすべてを表す

指示子いろいろ

  • 型で対象の Join Point を選択する
  • その他の方法

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.CompletableFutureorg.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

参考