Goslings開発メモ - その1: Spring Boot編」の続き。

Spring Boot続編で、DIについて。

DIとは

DIはDependency Injectionの略。依存性注入と訳される。

これは、Javaの文脈で具体的目に言うと、あるクラスが依存する具象クラスのインスタンス化と取得をフレームワークに任せることで、具象クラス間の直接的な依存を排除し、よってコンポーネント間を疎結合にする手法。 これにより、アプリの拡張性を高めたり、テストがしやすくなったりする。(参考記事)

Spring FrameworkはもともとこのDI機能を提供するフレームワーク(i.e. DIコンテナ)として普及した。

GoslingsでDI

Goslingsサーバの内部機能はざっくり、クライアントからのREST API呼び出しを処理するユーザインタフェース層と、Gitリポジトリにアクセスするデータベース層に分かれる。

Gitリポジトリにアクセスする部分は今回はJGitで実装するが、将来的に別のライブラリで実装しなおす可能性が微レ存なのと、Goslingsの開発自体がWebアプリ開発の練習でもあるので、ちゃんとしたアーキテクチャでと思い、DAOパターンを使ってやった。

つまり例えば、GitのコミットオブジェクトはJGitのAPIではRevCommitクラスで表されるが、ユーザインタフェース層からはリソースクラスであるCommitクラス(前回参照)を扱う以下の様なDAOインターフェースを呼ぶようにし、JGit依存の実装とは切り離す。

public interface ObjectDao {

  public Commit[] getCommits(String token) throws DaoException;

}

(ObjectDao.javaの完全なソースはこれ)


ObjectDaoを実装するObjectDaoImplクラスでは、以下の様にJGitを使ってごりごりと実装を書く。

public final class ObjectDaoImpl implements ObjectDao {

  // フィールド定義は省略

  @Override
  public Commit[] getCommits(String token) {
    try {
      return StreamSupport.stream(resolver.getGit(token).log().all().call().spliterator(), false)
               .map(this::convertToCommit)
               .toArray(Commit[]::new);
    } catch (NoHeadException e) {
      // エラー処理
    }
  }

  private Commit convertToCommit(RevCommit commit) {
    // RevCommitをCommitに変換する処理
  }

}


ユーザインターフェース層はRestApiV1Controllerクラス(前回参照)のgetCommitsメソッドで、以下の様にObjectDaoを使いたい。

public final class RestApiV1Controller {

  private ObjectDao objectDao;

  @RequestMapping(path="{token}/objects/commits")
  public Commit[] getCommits(@PathVariable String token) {
    return objectDao.getCommits(token);
  }

  // 以下他のメソッド

}


ここで問題になるのが、RestApiV1ControllerobjectDaoフィールドへのインスタンスの代入だが、RestApiV1Controller内(e.g. RestApiV1Controllerのコンストラクタ)でObjectDaoImplをインスタンス化して代入するのでは、ObjectDaoImplというデータベース層の具象クラスへの直接的な依存(i.e. import ObjectDaoImpl)が発生してしまってまずい。 ユーザインターフェース層とデータベース層が密に結合してしまう。

ここがDIの使いどころだ。 RestApiV1ControllerへのObjectDaoImplインスタンスの注入をフレームワークに任せればいい。

Spring BootでのDI

Spring BootアプリではSpring FrameworkのDI機能を何でも使えるが、普通、もっとも簡単な方法である@ComponentScan@Autowiredを使う方法を採る。

まずは@ComponentScanだが、これは、前回書いたように既に使っていて、プロジェクト内の全てのSpring Beanが検索されDIコンテナに登録されるようになっている。 なので、注入したいObjectDaoImplがSpring Beanと判定されるようにすればよい。

そのためには、ObjectDaoImplに以下のアノテーションのいずれかを付ける必要がある。

  • @Service: 業務手続を表すAPIを提供する(しばしば状態を持たない)コンポーネント。またはそれっぽいもの。MVCアーキテクチャのM(モデル)や、3層アーキテクチャのビジネスロジック層のコンポーネント。
  • @Repository: データの保持、取得、検索といった振る舞いを持つ、オブジェクトコレクションを表すコンポーネント。またはそれっぽいもの。MVCアーキテクチャのM(モデル)の内、特にデータベースを扱うコンポーネントや、3層アーキテクチャのデータベース層のコンポーネント。
  • @Controller: MVCアーキテクチャのC(コントローラ)のコンポーネント。
  • @Component: 一般的なコンポーネント。

(参考記事)


ObjectDaoImplはDAOコンポーネントで、これはもちろん@Repositoryにあたるのでこれを付ける。

@Repository
public final class ObjectDaoImpl implements ObjectDao {
  // 省略
}


これでObjectDaoImplがSpring Beanとして登録されるので、あとはRestApiV1Controller@Autowiredで注入してやればいい。

public final class RestApiV1Controller {

  @Autowired
  private ObjectDao objectDao;

  // 以下省略。

}


@Autowiredを付けたことにより、RestApiV1Controllerのインスタンス化直後に、objectDaoフィールドに適切なSpring Beanが注入されるようになった。

注入されるSpring Beanはフィールドの型から判断される。 objectDaoフィールドの型はObjectDaoで、この実装はプロジェクト内にObjectDaoImplしかないので、狙い通りObjectDaoImplが注入される。 今はこれでもいいが、将来ObjectDaoの実装が増えた場合、どの実装を注入すべきかSpring Frameworkには分からなくなるので、今のうちに@Qualifierを使って明示しておくことにする。(参考)

まずSpring Beanの方にjgitという値を持つ@Qualifierをつける。

@Repository
@Qualifier("jgit")
public final class ObjectDaoImpl implements ObjectDao {
  // 省略
}

(ObjectDaoImpl.javaの完全なソースはこれ)


Spring Beanを使う側にも同じ@Qualifierをつける。

public final class RestApiV1Controller {

  @Autowired
  @Qualifier("jgit")
  private ObjectDao objectDao;

  // 以下省略。

}

(RestApiV1Controller.javaの完全なソースはこちら)


これでRestApiV1ControllerobjectDaoフィールドにどのObjectDao実装が注入されるかがより明確になった。 将来ObjectDaoの別の実装を作るときには、その実装クラスには別の値の@Qualifierを付けてやれば、RestApiV1Controllerの方の@Qualifierの値によって注入する実装を切り替えられる。


今日はここまで。 次回もまたSpring Bootで、例外処理について。