Dependency Injection 失敗時の対応方法
ある技術を学ぶとき、成功例だけでなく失敗例も知っておくことは重要です。 よく使用する機能のコーディングミスは遭遇する確率が高いため、失敗例を知っておくことで無駄なデバック時間を減らすことができます。
このレクチャーでは、典型的な DI の失敗について紹介します。 出力されたエラーメッセージを読み解いて、適切な修正ができるようになりましょう。
目次
復習:DI のステップは「登録」と「取得」
まずは DI について復習をします。 DI は
- 「使いたいオブジェクト」を
- 「直接 new せず外部から渡してもらうようにする」』
デザインパターンであると紹介しました。 DI 経由で渡してもらうオブジェクトを Bean と呼びます。 DI を実現するためには
- Bean 登録
- Bean 取得
の2つのステップが必要です。 このいずれかが欠けていると DI は失敗します。
次のコードは Bean 登録の例です。
TaskService
クラスを Bean 登録するために、クラスに @Service
アノテーションを付与しています。
@Service
public class TaskService {
次のコードは Bean 取得の例です。
TaskController
クラスのコンストラクタの引数に TaskService
を定義しています。
このようにすると TaskController
の初期化時に、Bean 登録されている TaskService
がコンストラクタの引数に渡されます。
@Controller
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
失敗パターン1:Bean の登録漏れ
Bean の登録漏れのケースを見てみましょう。
次の TaskController
を考えます。
このコードでは、TaskService
を DI しています。
つまり、TaskService が Bean として登録されている必要があります。
@Controller
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
}
ここで、TaskService
を Bean 登録しないようにしてみます。
TaskService
に付与されていた @Service
アノテーションをコメントアウトしました。
// @Service // ← コメントアウト
public class TaskService {
// ...
}
この状態で Spring Boot を起動をすると、起動に失敗し以下のエラーメッセージが出力されます:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.todo.controller.task.TaskController required a bean of type 'com.example.todo.service.task.TaskService' that could not be found.
Action:
Consider defining a bean of type 'com.example.todo.service.task.TaskService' in your configuration.
このエラーメッセージで重要な箇所は次の1行です。
Parameter 0 of constructor in com.example.todo.controller.task.TaskController required a bean of type 'com.example.todo.service.task.TaskService' that could not be found
「TaskControllerのコンストラクタの第0引数には、TaskService の bean が必要ですが、見つからなかった」と言っています。
Spring が TaskController
をコンストラクタ TaskController(TaskService taskService)
を使って初期化しようとしますが、コンストラクタの引数として必要な TaskService
が Bean 登録されておらず起動に失敗しました。
このようなケースでは、見つからないと言われている bean が DI に登録されているかを調べることになります。
今回のケースですと、次のように TaskService
に @Service
アノテーションを付与することで解決できます:
@Service
public class TaskService {
// ...
}
失敗パターン2:Bean の取得漏れ
次は取得漏れのケースを再現してみます。 このケースでは Spring の起動には成功しますが、実行時にエラー (NullPointerException) が発生します。
以下のように TaskService
を初期化するコンストラクタを削除したコードを考えます。
@Controller
public class TaskController {
private TaskService taskService; // final 修飾子を削除した
// TaskService を初期化するコンストラクタを削除した
// public TaskController(TaskService taskService) {
// this.taskService = taskService;
// }
public String index() {
// taskService が null なので NullPointerException が発生する
var tasks = taskService.findAll();
}
}
この状態では、Spring Boot の起動には成功します。
しかし、TaskController
内で taskService のメソッドを呼び出したときに NullPointerException
が発生します。
java.lang.NullPointerException: Cannot invoke "com.example.todo.service.task.TaskService.find(com.example.todo.service.task.TaskSearchEntity)" because "this.taskService" is null
このようなエラーが発生した場合には、Bean の取得漏れである可能性を疑い、コンストラクタが定義されているかを確かめましょう。 今回は以下のように修正することで NullPointerException を取り除くことができます:
@Controller
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
public String index() {
// taskService に Bean が DI されているため
// taskService.findAll() を呼び出せる
var tasks = taskService.findAll();
}
}
まとめ:失敗も経験しておくことが大事
このレクチャーでは Dependency Injection の失敗例を紹介しました。 プログラミングにはエラーがつきものです。 エラーに遭遇したときに、原因も分からないままむやみにコードを書き換えることはよい手法ではありません。 今回紹介したようにエラーログを丁寧に読み解き、その原因を退治する打ち手を考えるようにしましょう。