この記事に書いてあること
- プロになるためのSpring入門の基本編のDIに関する内容を読んだまとめ
- DIを使ったコードと対応するテストコードの実装例
DIとは?
Dependency Injection(依存性の注入)の略。
ソフトウェアの設計パターンの一つで、クラス間の依存関係をフレームワーク等で管理して外部から注入する仕組みのこと。1
DIのメリット
- 依存オブジェクトの切り替えが簡単になり、コードの再利用性、可読性、テスト容易性が上がる
- 依存オブジェクトをインターフェースにすることでビジネスロジックから低レイヤーへの依存を切り離せる
具体例として保存処理や読み込み処理でServiceクラスがその処理の中でRepositoryクラスを利用してデータベース等に保存や読み込みを行う処理を考えてみます。
この時ServiceクラスはRepositoryクラスに依存していますが、Serviceクラスの処理の中で直接Repositoryクラスをインスタンス化すると差し替えが難しくなります。
差し替えが難しくなると色々な問題が発生します。
- Serviceクラスの単体テストを書く際にRepositoryクラスを動かすデータベースが必要になってテストサイズが大きくなってしまう
- Repositoryの実装が複数ある場合、依存オブジェクトを切り替えるためのロジックがServiceの中に現れて可読性が下がってしまう
またRepositoryの具象クラスを生成するということは、ビジネスロジックがデータベースやファイルシステムなどの低レイヤーの処理が記載されたクラスに依存している状態になります。
こういった良くない状態をDIによって解決することができます。
SpringでDIを実現する仕組み
DIコンテナ
SpringでDIの機能を提供するオブジェクトを入れて管理するための入れ物です。
ApplicationContextというインターフェースを実装しているため、ApplicationContextと呼ばれることもあります。
Bean
SpringのDIが管理するオブジェクトのことをBean2と言います。
ControllerやService、RepositoryなどのクラスをBeanとして指定することでSpringのDIの仕組みにのせて実装することができます。
コンフィグレーション
DIコンテナに読み込ませる情報のことをコンフィグレーションと言います。
コンフィグレーションには後述のBean定義の情報やDIコンテナの機能を有効化させるかどうかの設定を持っています。
開発者がコンフィグレーションを記述してDIコンテナに読み込ませることで、Beanとなるオブジェクトが生成されて依存オブジェクトがインジェクションされます。
コンフィグレーションは任意のJavaのクラスファイルに @Configuration
というアノテーションをつけることでDIコンテナにコンフィグレーションとして認識されます。
Bean定義
クラスをBeanとして指定する方法をBean定義と言います。
Bean定義には以下の3つの方法があります。
ステレオタイプアノテーション
ステレオタイプアノテーションはBeanとして管理してほしい具象クラスにつけるアノテーションです。ステレオタイプアノテーションが付いた具象クラスはDIコンテナに検知され、オブジェクトの生成や依存関係を管理されます。
ステレオタイプアノテーションにはいろいろな種類があり、対象のクラスに応じて使い分けます。
-
@Service
Serviceの具象クラスにつけるアノテーション、付加機能はなし。 -
@Repository
Repositoryの具象クラスにつけるアノテーション、データベースアクセス例外をSpringが提供する例外に変換できる付加機能が付与される。 -
@Controller
Controllerの具象クラスにつけるアノテーション、SpringMVCの機能を利用できるようになる付加機能が付与される。 -
@Component
役割を表さない汎用的なステレオタイプアノテーション、付加機能はなし。
コンポーネントスキャン
DIコンテナがステレオタイプアノテーションを付けたBeanを検出する処理のこと。
コンポーネントスキャンを有効にしたコンフィグレーションが読み込まれることでステレオタイプアノテーションを付けられたクラスがDIコンテナに登録されます。
コンフィグレーションの @Bean
メソッドで定義する
コンフィグレーションに以下のような記述をすることでDIコンテナにオブジェクトが登録されます。
@Configuration
public class TestConfig {
@Bean
public TestService testService() {
return new TestService();
}
}
@Bean
メソッドを使った定義はステレオタイプアノテーションと比べてやや煩雑になるため、大量の定義を行うのは大変ですが、ステレオタイプアノテーションを付与できないライブラリが提供するクラスもBean定義することができます。
xmlファイルで定義する
古い実装方法になりますが、xmlファイルにBeanの定義を記述することもできます。
<bean class="com.example.TestService" />
依存するクラスにBeanを注入する
@Autowired
というアノテーションを付けることで、依存するクラスにDIが管理しているBeanを注入することができます。
コンストラクタ、Setterメソッド、フィールドにつけることで型の一致するBeanを注入してくれます。
私は特別な理由がない場合コンストラクタによるインジェクションを使った方が良いと考えます。
理由は以下です。
- 循環参照を防止できる
- コンストラクタを使ってインジェクションすることでフィールドにfinal修飾子をつけることができるため、無用な変更を防止できる
- Setterがないクラスの方が外部から利用する際の手続きが減る
Setterを持ったクラスの場合
インスタンス生成⇒必要なオブジェクトをSet⇒主処理のメソッドを呼び出し
Setterのないクラスの場合
インスタンス生成⇒主処理のメソッドを呼び出し
DIコンテナの生成
DIコンテナの生成は明示的に行う必要があります。
mainメソッドの実行直後等に ApplicationContext
インターフェースの具象クラス(AnnotationConfigApplicationContextもその一つです)をインスタンス化することで生成されます。
public static void main(String[] args){
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
...
SpringBootの場合はSpringApplicationクラスのrunメソッドを使用します。
public static void main(String[] args){
ApplicationContext context = SpringApplication.run(TestConfig.class, args);
...
同じ型のBeanが複数存在した場合の挙動
DIコンテナの生成時にエラーが発生し、処理が中断されます。
プロファイルを利用したコンフィグレーションの切り替え
プロファイルとはコンフィグレーションをグルーピングするDIコンテナの機能です。
例えば本番環境用のコンフィグレーションとステージング環境のコンフィグレーションであったり、MySQL用のコンフィグレーションとOracle用のコンフィグレーションといった形でコンフィグレーションをグループ分けすることができます。
プロファイルの使用方法
@Profile
アノテーションを利用して識別子を付与します。
ステレオタイプアノテーションでの利用例
@Repository
@Profile("mysql")
public class MySQLTestRepository implements TestRepository{
...
@Repository
@Profile("oracle")
public class OracleTestRepository implements TestRepository{
...
有効にするプロファイルの指定
システムプロパティや環境変数で指定することができます。
システムプロパティで指定する場合javaコマンドの引数で指定します。
java -Dspring.profile.active=mysql TestApplication
環境変数で指定する場合は SPRING_PROFILE_ACTIVE
という環境変数でプロファイルの識別子を指定しておきます。
テスト用のモックを注入する方法
上述のようにDIを使って実装したプロダクションコードはテストコードを書く際にもDIを利用して注入がしやすい状態になっています。
SpringのDIの仕組みを使ってテスト用のプロファイルを切り替える等してインジェクションをすることもできますが、Mockitoでも仕組みが用意されています。
-
@Mock
@Spy
モックとして注入したいインスタンスの変数に付与することで、モックインスタンスを自動生成する。 -
@InjectMocks
テスト対象のインスタンスを入れる変数に付与することで、モックが注入されたインスタンスを自動生成する。
実装例
上記記事で準備したSpringBootの開発環境のサンプルコードをSpringのDIの仕組みを使った実装に書き換える。
ServiceクラスのテストコードもMockitoのインジェクションの仕組みを用いて書く。
DemoController
のコードを MessageMakeService
MessageRepository
に分解。
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class DemoController {
private final MessageMakeService messageMakeService;
public DemoController(MessageMakeService messageMakeService){
this.messageMakeService = messageMakeService;
}
@GetMapping("test")
public String showMessage(Model model) {
model.addAttribute("message", messageMakeService.make());
return "test";
}
}
package com.example.demo;
import org.springframework.stereotype.Service;
@Service
public class MessageMakeService {
private final MessageRepository messageRepository;
public MessageMakeService (MessageRepository messageRepository){
this.messageRepository = messageRepository;
}
public String make(){
return "メッセージは : " + messageRepository.loadMessage();
}
}
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class MessageRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public String loadMessage(){
String sql = "SELECT MESSAGE FROM TEST";
return jdbcTemplate.queryForObject(sql, String.class);
}
}
MessageMakeService
のテストコードを以下のように書く。
MessageRepository
をMockに差し替えた単体テストを簡単に記述することができている。
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
public class MessageMakeServiceTest {
@InjectMocks
private MessageMakeService testTarget;
@Mock
private MessageRepository messageRepository;
@Test
public void testMake(){
doReturn("モックのメッセージ").when(messageRepository).loadMessage();
assertThat(testTarget.make(), is("メッセージは : モックのメッセージ"));
}
}
DIコンテナの生成等は自動生成されるApplicationクラスが実行してくれています。3
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
おわり。
追記:
この記事の続きとして以下の記事を書きました。
SpringSecurityを使ってログインの仕組みを実装する記事も書きました。
よろしければこちらもあわせて読んでみてください。