Spring DI/DI コンテナについて
案件で SpringBoot を使用した開発をすることになりました。
作業していく中で DI(Dependency Injection)の考え方に詰まりました。
SpringBoot を使用し半年が経ち自分なりに DI の概念をある程度落とし込めたので、
調査結果を備忘録として残してみました。
目次
- DI とは
- DI コンテナとは
- DI コンテナのメリット
- コンポーネントスキャン
- DI コンテナの設定
- Bean 定義
- DI の記述
- テスト例
- まとめ
DI とは
Dependency Injection の略で日本語で言うと「依存性の注入」です。
Wikipedia で調べてみると「オブジェクトの注入」と書かれていました。(こちらのがわかりやすいですね)
クラスの外から依存性(オブジェクト)を注入することで、クラス間の依存関係を解決する目的を持っているそうです。
DI コンテナとは
DI を実現するためのフレームワーク。
DI コンテナを経由してインスタンスを生成、管理する。
※Spring では DI コンテナが管理するインスタンスのことを「コンポーネント」と表現します。
・DI コンテナイメージ図
出典:Spring 徹底入門
DI コンテナのメリット
- インスタンスのスコープ管理が可能
- インスタンスのライフサイクル制御が可能
- 共通機能を組み込むことが可能
- テストが簡単になる ※6.テストの実施例を記載しています。
コンポーネント間が疎結合となり、単体テストを行いやすくなる。
- DIの作法に従うことで、クラス内部でのインスタンス生成を行わなくなる(密結合の回避)
- インスタンスは外部から注入されるため、参照の切り替えが容易。
2.の要素により、テストコードでは注入するクラスをモックに置換すれば良いため、
クラス結合観点の確認が容易となる。
コンポーネントスキャン
コンポーネントスキャンとは、クラスローダーをスキャンして特定のクラスを自動的に DI コンテナに登録すること。
今回はコンポーネントスキャンの対象としてよく使う@Controller,@Service,@Component,@Repository について記載。
アノテーション | 説明 |
---|---|
@Controller | MVC パターンの C の役割を担うコンポーネントで、 このアノテーションを付与したコンポーネントではクライアントからのリクエスト/レスポンスに関わる処理をする。 ・@RestController との違い 大まかに View に遷移するかしないかの違いがあり、 @Controller は戻り値として View(HTML)を返すのに対して@RestController はリクエストを受け、JSON や XML を返す違いがある。 |
@Service | ビジネスロジックを実装するコンポーネント。 |
@Repository | データの永続化に関わる処理を提供するコンポーネント。ORM などを利用して、データの CRUD 処理を実装する。 |
@Component | 上記 3 つに当てはまらないコンポーネント。ユーティリティクラスなどに付与する。 thymeleaf のテンプレートの機能だけでは賄いきれない HTML への変換ロジックを使いたい時などに便利。 |
他にもデフォルトで以下のアノテーションが付いたクラスが対象になる。
- @Configuration
- @RestController
- @ControllerAdvice
- @ManagedBean
- @Named
DI コンテナの設定
1. Bean 定義
Spring では DI コンテナに登録するコンポーネントのことを「Bean」、Bean の構成を定義することを「Bean 定義」と言います。
今回はアノテーションベースの定義で書かせていただきます。(XML ベース,Java ベースの定義は今回は扱いません。)
Bean 定義ファイルに記述するのではなく、DI コンテナに管理させたい Bean もしくはコンポーネントにアノテーションを付与し、
読み込むことで DI コンテナに登録する方法です。
記述方法は以下です。
・Bean クラスに@Component アノテーションを付与して、コンポーネントスキャンの対象にします。
@Component
public class SampleController {
2. DI の記述
DI の記述方法の種類は以下の 3 つがあります。
- セッターインジェクション
セッターの引数に対して DI する方法。- メリット :テスト時にモックオブジェクトに置き換えやすい
- デメリット:依存する Bean が多いと setter が多くなる
@Controller
public class SampleController {
// 利用するBeanのフィールドを定義する
private SampleService SampleService;
// Bean設定用のsetterを@Autowiredを付与して定義する
@Autowired
public void setSampleService(SampleService sampleService) {
this.sampleService = sampleService;
}
public void fuga() {
sampleService.hoge();
}
}
- コンストラクタインジェクション
コンストラクタの引数に対して DI する方法。- メリット :フィールドに final 修飾子を付けて不変に出来る。
- デメリット:依存する Bean が増えるにつれて引数が多くなる
@Controller
public class SampleController {
// 利用するBeanのフィールドを定義する
private final SampleService sampleService;
// @Autowiredを付与したコンストラクタを用意する(引数はBeanオブジェクト)
// コンストラクタが1つの場合、@Autowiredは省略可
@Autowired
public SampleController(SampleService sampleService) {
this.sampleService = sampleService;
}
public void hoge() {
// Beanを利用する
sampleService.foo();
}
}
- フィールドインジェクション
クラスのフィールドに対して DI する方法。- メリット :コード量が少なくて一番シンプルに書ける。
- デメリット:フィールドを final 化できない
@Controller
public class SampleController {
// 利用するBeanのフィールドを@Autowiredを付与して定義する
@Autowired
private SampleService sampleService;
public void hoge() {
// Beanを利用する
sampleService.foo();
}
}
テスト例
以下に例を記載します。
例 Repository がまだ実装されていない段階での Service のテスト
パッケージ構成
TaskController.java
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.service.TaskService;
@Controller
@RequestMapping("/task")
public class TaskController {
@Autowired
private TaskService taskService;
@GetMapping
public String task(Model model) {
model.addAttribute("taskList", taskService.findAll());
return "index";
}
}
Task.java
package com.example.demo.dto;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class Task {
private int id;
private int typeId;
private String title;
private String detail;
private LocalDateTime deadline;
private int userId;
}
TaskDao.java
package com.example.demo.repository;
import java.util.List;
import com.example.demo.dto.Task;
public interface TaskDao {
List<Task> findAll();
}
今回のテスト対象クラス
TaskService.java
package com.example.demo.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.dto.Task;
import com.example.demo.repository.TaskDao;
@Service
public class TaskService {
@Autowired
private TaskDao dao; /** 実装クラスが未実装*/
public List<Task> findAll() {
return dao.findAll();
}
}
テストクラス
TaskServiceTest.java
package com.example.demo.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
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 com.example.demo.dto.Task;
import com.example.demo.repository.TaskDao;
@ExtendWith(MockitoExtension.class)
@DisplayName("TaskServiceImplのテスト")
public class TaskServiceTest {
@Mock
private TaskDao dao;
@InjectMocks
private TaskService taskService;
@Test
@DisplayName("テーブルtaskの全件取得で0件の場合のテスト")
void testFindAllList() {
List<Task> list = new ArrayList<>();
when(dao.findAll()).thenReturn(list);
List<Task> actualList= taskService.findAll();
verify(dao, times(1)).findAll();
assertEquals(0, actualList.size());
}
}
TaskService.java で TaskDao を@Autowired することにより、TaskDao のインスタンスは常に1つだけしか存在しない状態(=シングルトンパターン)での実装となります。
シングルトンパターンについては今回の記事の主題とは異なりますので扱いません。ご興味のある方は下記の参考資料をご覧ください。
まとめ
今回は DI,DI コンテナについて書かせていただきました。
これから Spring Framework を使おうと考えている方の参考になれば幸いです。
引用元
- Reasonable Code
Spring Framework で DI する 3 つの方法 - Qiita
Spring Boot 入門 ① ~ DI ~
Spring Framework 要点まとめ ~ DI について - wikipedia
依存性の注入