SpringのDI(依存性注入)は、クラスが使う部品(依存オブジェクト)を自分でnewせず、Springから受け取る仕組みです。言葉で読むと抽象的なので、この記事ではDIなし・ありのコードを並べ、Springのコンテナを実際に起動したときの出力を見ながら説明します。掲載コードは Spring Framework 7.0.9(Spring Boot 4.1.1 が採用している版)と JDK 25 で実行し、出力とエラー文はその結果をそのまま載せています。
まとめ:SpringのDIで押さえる5つの要点
- DIとは:依存する相手を自分で
newせず、外から受け取る設計。Springでは DIコンテナ(ApplicationContext)が「Bean」を生成し、必要な場所へ渡す。 - 注入方法はコンストラクタ注入が基本:Springの公式リファレンスがコンストラクタ注入を推奨し、セッター注入は主に妥当な既定値を持つ任意の依存向けとしている。本記事では、新規コードにフィールド注入を採用しない方針とする。
- 1つだけ受け取る注入先に同じ型のBeanが2つあると起動に失敗する:
NoUniqueBeanDefinitionExceptionになる。@Primary・@Qualifier・@Fallback(6.2以降)で選ぶ。 - Beanは既定でシングルトン:同じコンテナ内の同じBean定義では1つのインスタンスを共有するので、利用者ごとの状態をフィールドに持たせない。
- 循環参照はSpring Bootでは既定で起動エラー:Spring Boot 2.6 以降の挙動。設定で許可せず、依存の向きを直す。
SpringのDI(依存性注入)の意味:必要な部品を外から受け取る設計
DI(Dependency Injection=依存性注入)の「依存」は「AがBを使っている(AはBがないと動かない)」関係を、「注入」はそのBを外から差し込むことを指します。料理に例えると、シェフ(クラス)が食材(依存オブジェクト)を自分で畑に採りに行くのがDIなし、仕入れ担当(Springのコンテナ)が食材を届けてくれるのがDIありです。シェフは「何を使うか」だけを宣言し、「どこからどう調達するか」は気にしません。
DIなしのコードとDIありのコードの違い
DIを使わない場合、UserServiceは依存先のUserRepositoryを自分で生成します。
public class UserService {
private final UserRepository userRepository = new JdbcUserRepository();
public void register(String name) {
userRepository.save(name);
}
}
この書き方ではUserServiceがJdbcUserRepositoryという具体的な実装に固定されます。テストでDBを使わない偽物に差し替えたくても、内部で生成しているので外から置き換えられません。
DIを使う場合は、依存先をコンストラクタの引数として受け取ります。
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String name) {
userRepository.save(name);
}
}
変わったのは「作る」から「受け取る」への一点だけです。UserServiceはインターフェースUserRepositoryにしか依存しなくなり、本番ではDB実装を、テストでは偽物を渡せます。この「渡す」役をSpringが自動で担うのが、SpringのDIです。
DIとIoC(制御の反転)・DIコンテナの関係
DIはIoC(Inversion of Control=制御の反転)とセットで語られます。オブジェクトの生成と組み立てを自分のコードからフレームワークへ移すのがIoCで、DIはその代表的な実現方法です。DIという呼び名は、Martin Fowler が2004年1月に公開したInversion of Control Containers and the Dependency Injection patternで広まりました。同記事は「Separating Configuration from Use」という節を設け、サービスの設定(configuration)を利用(use)から分離することが要点だと述べています。どの実装を使うかという「設定」を、それを使うクラスの外に出す。DIの価値はこの一点に集約されます。
IoCコンテナの内部(BeanFactoryとApplicationContextの違い、Beanのスコープとライフサイクル)はIoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanをわかりやすく解説で掘り下げています。DIと名前の似た設計原則DIP(依存性逆転の原則)との違いは依存性の注入(DI)と依存性逆転の原則(DIP)の違い|コード例で使い分けを参照してください。
DIコンテナを動かして仕組みを確かめる最小構成
SpringのDIは Spring Boot がなくても動きます。spring-context(Spring Boot のスターターにも含まれるコアの部品)だけで、コンテナの動きを確認できます。Spring Boot で新しくプロジェクトを作る手順はSpring Bootとは|4.1系の依存名の変更と最小Web APIの作り方にまとめています。
@Configuration・@BeanによるBean定義とコンテナ起動
Mavenならpom.xmlのdependenciesに次の1つを足すだけで準備は終わりです。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>7.0.9</version>
</dependency>
次のコードは、@Configurationを付けた設定クラスに「どの実装を使うか」を書き、AnnotationConfigApplicationContextでコンテナを起動します。UserService自身はInMemoryUserRepositoryの存在を知りません。
package demo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
interface UserRepository {
void save(String name);
}
class InMemoryUserRepository implements UserRepository {
InMemoryUserRepository() {
System.out.println("1. InMemoryUserRepository を生成");
}
public void save(String name) {
System.out.println("保存: " + name);
}
}
class UserService {
private final UserRepository userRepository;
UserService(UserRepository userRepository) {
System.out.println("2. UserService を生成(依存を受け取る)");
this.userRepository = userRepository;
}
void register(String name) {
userRepository.save(name);
}
}
@Configuration
class AppConfig {
@Bean
UserRepository userRepository() {
return new InMemoryUserRepository();
}
@Bean
UserService userService(UserRepository userRepository) {
return new UserService(userRepository);
}
}
public class App {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("3. コンテナの起動完了");
UserService first = context.getBean(UserService.class);
UserService second = context.getBean(UserService.class);
first.register("tanaka");
System.out.println("同じインスタンスか: " + (first == second));
}
}
}
src/main/java/demo/App.javaに保存し、mvn compile exec:java -Dexec.mainClass=demo.Appで実行した結果は次のとおりです。
1. InMemoryUserRepository を生成
2. UserService を生成(依存を受け取る)
3. コンテナの起動完了
保存: tanaka
同じインスタンスか: true
DIコンテナ起動時の依存解決・Bean生成・共有
この5行に、DIコンテナの仕事がそのまま表れています。
- 依存される側から先に生成する:
userServiceメソッドの引数がUserRepositoryなので、コンテナは先にInMemoryUserRepositoryを作り(1行目)、それを渡してUserServiceを作ります(2行目)。順番をコードに書いていないのに、引数の型から組み立て順が決まります。 - 起動の時点で全部作り終える:「3. コンテナの起動完了」より前に2つのBeanが生成されています。
ApplicationContextは既定で、シングルトンのBeanを起動時にまとめて生成します。設定の誤りが、最初のリクエストではなく起動時に見つかるのはこのためです。 - 何度取り出しても同じインスタンス:
getBeanを2回呼んでもtrueです。Springの公式リファレンスはBean Scopesで「The singleton scope is the default scope in Spring.」と明記しています。
Beanを登録する4つの方法
上の例は@Beanメソッドで登録しましたが、実務ではクラスに@Component系のアノテーションを付けて自動検出させる方法が中心です。Spring Framework 7.0 では、条件に応じてコードでBeanを登録するBeanRegistrarが加わりました(公式リファレンスProgrammatic Bean Registration)。
| 登録方法 | 書き方 | 向いている対象 |
|---|---|---|
| コンポーネントスキャン | クラスに@Component・@Serviceなど |
自作クラスの大半 |
| Java Config | @Configuration内の@Beanメソッド |
外部ライブラリのクラス |
| BeanRegistrar | BeanRegistrarを実装し@Import |
環境で登録内容を変えるとき(7.0以降) |
| XML設定 | XMLファイルのbean定義 | 既存システムの保守 |
新規開発では、自作クラスはコンポーネントスキャン、手を入れられない外部クラスは@Beanメソッドで登録すれば足ります。BeanRegistrarはライブラリやフレームワークを作る側の道具で、アプリケーションのコードで最初に覚える必要はありません。
Springの3つの注入方法と使い分け
Beanをどの経路で受け取るかには、コンストラクタ注入・セッター注入・フィールド注入の3つがあります。選び方の結論は公式リファレンスのDependency Injectionの章にあり、迷う余地はほとんどありません。
コンストラクタ注入:必須依存の明示と初期化
公式リファレンスは「The Spring team generally advocates constructor injection」と書き、理由として、部品を不変(immutable)にでき、必須の依存がnullにならず、完全に初期化された状態で渡されることを挙げています。
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
コンストラクタが1つだけなら@Autowiredは書かなくても注入されます。これは Spring Framework 4.3(2016年)で入った挙動で、当時のSpringブログの告知で紹介されています。コンストラクタが複数あるときは、どれを使うかを@Autowiredで指定します。
引数が5個、6個と増えてきたら、注入方法ではなくクラスの責務を疑ってください。公式リファレンスも、コンストラクタ引数が多いことを「クラスが責務を持ちすぎている」兆候としています。
セッター注入:任意依存を中心とした使い分け
公式リファレンスは、セッター注入を「クラス内で妥当な既定値を割り当てられる任意の依存」に主に使うべきだとしています。セッターに付けた@Autowiredも既定では必須扱いで、依存が見つからなければBeanの生成に失敗します。任意の依存をnullのまま許す場合は、利用時のチェックが必要です。
@Service
public class ReportService {
private MailSender mailSender; // 無ければメール送信を省く
@Autowired(required = false)
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
}
フィールド注入を新規コードで避ける3つの理由
フィールドに直接@Autowiredを付ける書き方は短く書けますが、新規コードでは使いません。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
- finalにできない:生成後に書き込まれるため、不変にできません。
- 依存をコンストラクタから渡してテストできない:外から依存を渡す口がないので、単体テストでもコンテナかリフレクションが必要になります。
- 循環参照が素のSpringでは通ってしまう:後述のとおり、素の Spring Framework ではフィールド注入どうしの循環参照が黙って成立します。コンストラクタ注入なら起動時に必ず失敗します。
既存コードがフィールド注入なら、そのクラスを修正するついでにコンストラクタ注入へ移します。
同じ型のBeanが複数あるときの選び方:@Primary・@Qualifier・@Fallback
DIで最初につまずきやすいのが「同じインターフェースの実装が2つある」場面です。次のように通知手段を2つ登録し、OrderServiceにNotifierを1つ渡そうとすると、コンテナはどちらを渡すか決められません。
interface Notifier {
String channel();
}
record OrderService(Notifier notifier) {
}
@Configuration
class NotifierConfig {
@Bean
Notifier mailNotifier() {
return () -> "mail";
}
@Bean
Notifier slackNotifier() {
return () -> "slack";
}
@Bean
OrderService orderService(Notifier notifier) {
return new OrderService(notifier);
}
}
起動はUnsatisfiedDependencyExceptionで失敗し、その原因として次のNoUniqueBeanDefinitionExceptionが記録されます。
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'demo.Notifier' available: expected single matching bean but found 2: mailNotifier,slackNotifier
メッセージ末尾の「found 2: mailNotifier,slackNotifier」が候補のBean名です。解決方法は3つあり、同じ構成で付け替えて実行した結果は次のとおりでした。
| 付けたもの | 付ける場所 | 渡されたBean |
|---|---|---|
| なし | – | 起動失敗(上の例外) |
@Primary |
mailNotifier の定義 | |
@Primary+@Qualifier("slackNotifier") |
Primaryは定義、Qualifierは受け取る引数 | slack |
@Fallback |
slackNotifier の定義 |
使い分けは次のとおりです。
@Primary:「普段はこれ」と決まっている既定の実装に付けます。@Qualifier:受け取る側が個別に指定します。上の3行目では、@Qualifierが候補をslackNotifierに絞るため、mailNotifierの@Primaryは選択に影響しません。@Fallback:Spring Framework 6.2 で追加された、@Primaryの逆です。「ほかに候補が無いときだけ使う」実装に付け、残りが1つならそれが選ばれます(公式リファレンスFine-tuning Annotation-based Autowiring with @Primary or @Fallback)。
どれを選ぶかの判断基準は「誰が知っているべきか」です。既定の実装を設定側で決めたいなら@Primary、特定のクラスだけ別の実装を使うなら@Qualifierにします。全実装を受け取りたいときはList<Notifier>で受け取れば、登録された全Beanが渡されます。
DIで単体テストが書きやすくなる理由
DIのメリットとして「テストしやすい」とよく書かれますが、具体的にはSpringを起動せずに、テスト用の偽物をnewで渡せることです。ここでは、名前が空白なら保存せずに例外を投げるUserServiceをテストします。
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name is blank");
}
userRepository.save(name);
}
}
コンストラクタ注入で書いてあるので、JUnit だけで次のように検証できます。
package demo;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
// テスト用の偽物。DBにはつながない
static class FakeUserRepository implements UserRepository {
final List<String> saved = new ArrayList<>();
public void save(String name) { saved.add(name); }
}
@Test
void registerSavesUser() {
var repository = new FakeUserRepository();
var service = new UserService(repository); // Springを起動せず new で渡す
service.register("tanaka");
assertEquals(List.of("tanaka"), repository.saved);
}
@Test
void blankNameIsRejected() {
var repository = new FakeUserRepository();
var service = new UserService(repository);
assertThrows(IllegalArgumentException.class, () -> service.register(" "));
assertTrue(repository.saved.isEmpty());
}
}
テストはsrc/test/java/demoに置きます。Spring Boot 4.1.1 のテスト用スターター(JUnit 6.0.3 を含む)を依存に入れてmvn testで実行すると、2件とも成功し、所要時間は2件合わせて0.1秒でした。コンテナを起動しないので速く終わります。
偽物のクラスを書く代わりに、Spring Boot のテスト用スターターに含まれる Mockito でmock(UserRepository.class)を渡す方法もあります。偽物とモックの使い分けはモックとは?スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説、JUnitの基本はJUnitとは?Javaの単体テストフレームワークの基本と書き方【2026年版】を参照してください。
SpringのDIでつまずきやすい2つの落とし穴
シングルトンBeanにおける利用者別の状態共有リスク
Beanは既定でシングルトンなので、フィールドに値を持たせると全利用者で共有されます。
@Component
class VisitCounter {
private int count = 0; // シングルトンなので全利用者で共有される
int visit() {
return ++count;
}
}
利用者Aと利用者Bがそれぞれ1回ずつvisit()を呼ぶと、出力は次のようになります。
利用者Aの1回目: 1
利用者Bの1回目: 2
Bにとっては1回目なのに2が返ります。Webアプリでは複数のリクエストが同じBeanを同時に使うため、ログインユーザー名や処理中の値をフィールドに入れると、別の利用者の値が混ざります。公式リファレンスも「状態を持つBeanにはprototypeスコープ、状態を持たないBeanにはsingletonスコープを使う」としています。サービスクラスは状態を持たない作りにし、値はメソッドの引数と戻り値で受け渡します。サービス層の書き方はサービスクラスとは?Java・Springでの役割と書き方、コントローラーとの違いで解説しています。
循環参照はSpring Bootでは既定で起動エラー
AがBを、BがAを必要とする循環参照は、注入方法と、Spring Boot を使うかどうかで結果が変わります。同じ2クラスで試した結果は次のとおりです。
| 構成 | 素のSpring Framework 7.0.9 | Spring Boot 4.1.1 |
|---|---|---|
| コンストラクタ注入どうし | 起動失敗 | 起動失敗 |
| フィールド注入どうし | 起動成功(循環が残る) | 起動失敗 |
コンストラクタ注入では、AとBのどちらも相手より先に作れないため、どの構成でもBeanCurrentlyInCreationExceptionになります。フィールド注入では素の Spring Framework がいったん作りかけのBeanを渡して循環を成立させてしまいますが、Spring Boot は2.6から循環参照を既定で禁止しました(Spring Boot 2.6 Release Notes)。Spring Boot 4.1.1 では次の表示で起動が止まります。
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| bootCircular.X (field demo.BootCircular$Y demo.BootCircular$X.y)
↑ ↓
| bootCircular.Y (field demo.BootCircular$X demo.BootCircular$Y.x)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
メッセージにあるとおり、spring.main.allow-circular-references=trueは「最後の手段」です。設定で黙らせず、両方が使っている処理を第3のクラスへ切り出して、依存の向きを一方向にそろえます。
DIで使う主要アノテーションの早見表
DIまわりで最初に覚えるアノテーションは次の範囲です。
| アノテーション | 付ける場所 | 役割 |
|---|---|---|
@Component |
クラス | 自動検出の対象にする汎用の印 |
@Service |
クラス | 業務ロジック層のBean |
@Repository |
クラス | データアクセス層。設定により例外変換の対象 |
@Controller |
クラス | Web層のBean |
@Configuration・@Bean |
クラス・メソッド | メソッドの戻り値をBeanにする |
@Autowired |
コンストラクタなど | 注入の指示(コンストラクタ1つなら省略可) |
@Primary・@Qualifier・@Fallback |
定義・引数 | 同じ型の候補から選ぶ |
@Service・@Repository・@Controllerはどれも@Componentの一種で、Beanとして登録される点は同じです。層を示すことで設計の意図が読み手に伝わり、@Repositoryはデータアクセス例外の変換対象にもなります。各アノテーションの細かな挙動はSpringアノテーションとは?仕組みと主要アノテーションの使い分けを解説、Spring Boot 全体の一覧はSpring Bootのアノテーション一覧|役割別の早見表とBoot 4での変更点にあります。
Spring以外のDIとの違い:Jakarta CDI・Guice・Dagger
DIはSpring固有の考え方ではありません。Javaでは、Jakarta EE の標準仕様である Jakarta CDI(Contexts and Dependency Injection)、Google の軽量ライブラリ Guice、コンパイル時に注入コードを生成する Dagger(Androidで広く使われる)があります。Java以外でも、Angular や NestJS はフレームワーク自体にDIを組み込んでいます。
これらと比べたSpringの特徴は、DIコンテナを土台に、Web・データアクセス・セキュリティ・バッチまでを同じBeanの仕組みで揃えている点です。Spring Framework の全体像とSpring Bootとの関係はSpring Frameworkとは?特徴・DI/AOPとSpring Bootとの違い・最新版7を解説【2026年最新】で整理しています。業務システムを Java と Spring で新規に作る場合の進め方は、フルスクラッチ開発の支援内容も参考にしてください。
よくある質問
DIとDIコンテナは何が違いますか?
DIは「依存を外から渡す」という設計の考え方で、DIコンテナはそれを実際に行うSpringの部品(ApplicationContext)です。DIはコンテナがなくても、コンストラクタに手でnewしたものを渡せば成り立ちます。コンテナは、その組み立てを依存関係の数が増えても自動でこなす役割です。
SpringでDIを使うメリットは何ですか?
主なメリットは2つです。実装を差し替えるときに使う側のクラスを修正しなくてよいこと、そしてSpringを起動せずに偽物の依存を渡して単体テストできることです。加えて、既定の非遅延シングルトンBeanは起動時に生成・組み立てされるため、依存関係の設定誤りを起動時に検出できます。遅延初期化や他のスコープでは、生成時まで検出されない場合があります。
@Autowiredは必ず書く必要がありますか?
コンストラクタが1つだけのクラスなら不要です(Spring Framework 4.3以降)。複数のコンストラクタから注入先を明示する場合は@Autowiredを使います。どれにも付けていなくても、引数なしのコンストラクタがあればそれが使われます。セッターやフィールドには、@Autowiredのほか@Injectや@Resourceも利用できます。
@Componentと@Serviceはどう違いますか?
どちらもクラスをBeanとして登録します。@Serviceは@Componentの一種で、業務ロジック層であることを示す意味付けが違いです。層が決まっているクラスには@Service・@Repository・@Controllerを使い、どの層にも当てはまらない部品に@Componentを使います。
Spring BootとSpring FrameworkでDIの書き方は変わりますか?
アノテーションと注入方法は同じです。Spring Boot は@SpringBootApplicationを付けたクラスのパッケージ以下を自動でスキャンし、ライブラリのBeanも自動構成で登録します。挙動の差として、Spring Boot は循環参照を既定で禁止している点に注意してください。