IntelliJ IDEAでフィールドに @Autowired を付けると「Field injection is not recommended」という警告が出ますよね。新人研修ではフィールドインジェクションで習ったのに、現場のコードレビューでは「コンストラクタインジェクションに直してください」と言われる。なぜ方式によって扱いが違うのか、整理しておきましょう。

この記事を読み終えると、3方式の差をテスト容易性・null安全性・不変性・循環依存検知の4軸で説明できるようになり、新規コードで自信を持って書き分けられるようになります。DIそのものの概念がまだ曖昧という方は DIとは何か を先にどうぞ。

3方式とは

Spring Bootには依存Beanを受け取る方法が3つあります。コンストラクタ引数で受け取る コンストラクタインジェクション 、セッターメソッド経由の セッターインジェクション 、フィールドに直接 @Autowired を付ける フィールドインジェクション です。

Spring 4.3以降はコンストラクタが1つしかない場合、@Autowired を省略してもDIが効くようになりました。現代のSpring Bootコードで @Autowired をあまり見かけないのはこのためです。

同じサービスを3パターンで書いてみる

UserRepository に依存する UserService を題材にします。まずはフィールドインジェクションから。

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User find(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

次にセッターインジェクション。

@Service
public class UserService {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User find(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

そしてコンストラクタインジェクション。@Autowired は省略しています。

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User find(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

見た目だけだとフィールドインジェクションが一番短くて魅力的に映ります。でも重要なのは「何が保証されるか」です。

テスト容易性の違い

コンストラクタインジェクションが推奨される最大の理由はここにあります。ユニットテストで new するだけでモックを差し込めるんです。

@Test
void findユーザー() {
    UserRepository repo = mock(UserRepository.class);
    when(repo.findById(1L)).thenReturn(Optional.of(new User(1L, "taro")));

    UserService service = new UserService(repo);

    assertThat(service.find(1L).getName()).isEqualTo("taro");
}

フィールドインジェクションだと private フィールドに直接モックを入れる必要があり、リフレクションを使うか @SpringBootTest で起動するしかありません。テストが重くなりますし、Springに依存しないPOJOテストが書けなくなります。

セッターインジェクションは setUserRepository(...) を呼べばいいのですが、依存が増えるたびに呼び出しを忘れて NPE を踏むことが結構あります。

null安全性と不変性

コンストラクタインジェクションでは final を付けられます。final フィールドはコンストラクタ呼び出し時点で必ず初期化されるため、生成後の UserService インスタンスは依存が null の状態を取りえません。

フィールドインジェクションやセッターインジェクションだと、Springが値をセットする前にメソッドが呼ばれる可能性が文法上残ります。コードを読む側にとって「いつ依存が揃うか」が不透明なんですよね。

さらに必須依存が足りないときの挙動も違います。コンストラクタインジェクションなら アプリケーション起動時に失敗 するため、デプロイ前に気づけます。フィールド注入では実行時に初めて気づくケースがあります。

循環依存はいつバレるか

AServiceBService を、BServiceAService を必要とするような循環依存。コンストラクタインジェクションだと起動時に BeanCurrentlyInCreationException で落ちます。

The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  aService defined in file [.../AService.class]
↑     ↓
|  bService defined in file [.../BService.class]
└─────┘

一方フィールド/セッターインジェクションでは、Springが片方を先に生成して後から差し込むため、循環していても起動できてしまうことがあります。実行時にNPEや無限ループで顕在化する分、原因追跡が大変です。

設計問題は早く気づけるほどいい。Spring Boot 2.6からは循環依存がデフォルトで禁止になりました。これも「循環があったら設計を見直そう」というメッセージです。

Lombokの@RequiredArgsConstructorで楽をする

コンストラクタインジェクションの唯一の難点はボイラープレートでしたが、Lombokの @RequiredArgsConstructor を使えば消えます。

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final NotificationService notificationService;

    public User find(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

final フィールドだけを引数に取るコンストラクタが自動生成されます。依存を増やすときも private final ... を1行足すだけで済むので、変更箇所が最小です。Lombokの導入手順は Lombok の使い方 を参照してください。

IntelliJの警告メッセージの根拠

「Field injection is not recommended」の根拠は3つあります。1つ目はテストの困難さ、2つ目は final が使えず不変性を保てないこと、3つ目は依存がコンストラクタシグネチャに現れず隠れてしまうこと。

既存コードを書き換えるときは機械的でOKです。フィールドの @Autowired を外して final を付け、クラスに @RequiredArgsConstructor を付ける、これだけで警告が消えます。

セッターインジェクションが活きる場面

コンストラクタ一辺倒というわけでもなく、セッターが向く場面もあります。1つはオプショナル依存。

@Autowired(required = false)
public void setMetricsClient(MetricsClient client) {
    this.metricsClient = client;
}

もう1つは、設計上どうしても循環が外せない場合の 最終手段 として。ただしこれは後述の通り、本来は設計を直すべきです。

循環依存は設計で解く

@Lazy を付ければ循環依存は表面上は通ります。

public AService(@Lazy BService bService) {
    this.bService = bService;
}

でも応急処置です。本来は責務を分けるべきサイン。よくあるパターンは、両者が必要とする共通ロジックを第3のBeanに切り出すリファクタリングです。あるいは双方向の呼び出しを ApplicationEventPublisher 経由のイベントに置き換えれば、コンパイル時依存が一方向になります。

コンポーネントの責務分割は @ComponentとはBeanスコープの解説 も合わせて読むと整理しやすいです。

まとめ

新規コードは原則 コンストラクタインジェクション + @RequiredArgsConstructor を選びましょう。テストが書きやすく、null を取らない不変オブジェクトになり、循環依存も起動時に検知できます。

セッターはオプショナル依存に限って検討、フィールド注入はテストクラスや使い捨てのスクリプト的なコードに留める、というのが現実的な落としどころです。IntelliJの警告は設計を整えるためのリマインダーだと思って、見かけたら淡々と直していきましょう。