Table of Contents
「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);
}
// 以下他のメソッド
}ここで問題になるのが、RestApiV1ControllerのobjectDaoフィールドへのインスタンスの代入だが、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の完全なソースはこちら)
これでRestApiV1ControllerのobjectDaoフィールドにどのObjectDao実装が注入されるかがより明確になった。
将来ObjectDaoの別の実装を作るときには、その実装クラスには別の値の@Qualifierを付けてやれば、RestApiV1Controllerの方の@Qualifierの値によって注入する実装を切り替えられる。
今日はここまで。 次回もまたSpring Bootで、例外処理について。