1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これは何?

最近Springの国にやってきたのだが、知らないアノテーションだらけでなんぞ??という気持ちになっていた。
自分の場合DIコンテナについてわかってきたところで少し、見通しがよくなってきた気がする。

自分の理解を高めるのを兼ねつつ、自分のようなSpring初学者向けのDIのまとめ記事を作ったので役に立てば嬉しい。

この記事を読むとわかること

SpringにおけるDIの基礎知識

  • DIをする意味
  • DI関連の基本用語
  • DIに関連するSpringの機能

この記事は人間が書いています。
サンプルコードはClaude Codeが作成したのち、人間(Spring初心者)がレビューしています。


DI(依存性注入)とは

Martin Fowlerの記事1によると依存性注入(Dependency Injection, DI)とは制御の反転(IoC)という概念を具体化したパターンである。
制御の反転とは、依存オブジェクトの生成・管理の制御を、アプリケーションコード自身ではなく外部(Assembler2/(DI)コンテナ3)に委ねる考え方である。
インターフェースの実装を切り替えやすくなり、テストもしやすくなる。


DIをするメリット・目的

依存オブジェクトの実装の差し替えが容易になり、コードが疎結合になることが最大のメリットである。コードが疎結合になることで

  • スタブやモックに差し替えやすくなるため、テストコードが書きやすくなる、本番環境用とテスト環境用で実装の切り替えが容易になる
  • 明示的に依存関係を記載できるのでコードの可読性が上がる
    • オブジェクト自身が依存関係の詳細を知らなくてすむ

といったメリットがある。


SpringにおけるDI

Martin FowlerはDIの方法としてConstructor / Setter / Interface Injection の3種類を挙げている1。ただしSpringでは主に Constructor InjectionとSetter Injectionが一般的に使われる。

Springの公式ドキュメント4では、具体例として以下の3つが挙げられている。

  • コンストラクタ引数(Constructor Injection)
  • ファクトリメソッドの引数(Constructor Injectionに近い)
  • インスタンスが生成された後、もしくはファクトリメソッド返された後に設定されるプロパティを通して(Setter Injection)

DIコンテナ

DIコンテナを使わない例

SpringではDIコンテナを使用することが多いため、まずDIコンテナを使用しないパターンと比較してみる。

class Movie {
}

interface MovieRepository {
    void save(Movie movie);
}

class JdbcMovieRepository implements MovieRepository {
    public void save(Movie movie) {
        System.out.println("JDBC: save to DB");
    }
}

class MockMovieRepository implements MovieRepository {
    public void save(Movie movie) {
        System.out.println("Mock: save (no-op)");
    }
}

class MovieService {
    private final MovieRepository movieRepository;

    public MovieService(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    public void register(Movie movie) {
        movieRepository.save(movie);
    }
}

public class ManualDIExample {
    public static void main(String[] args) {
        // 依存オブジェクトを生成し、注入する
        new MovieService(new JdbcMovieRepository()).register(new Movie());
        new MovieService(new MockMovieRepository()).register(new Movie());
    }
}

このように、依存オブジェクトを生成し注入することで実装を切り替えられる。

DIコンテナを使用する例(Spring)

しかし、アプリケーションの規模が大きくなると用意するべき依存オブジェクトの数が大量になってしまう。
一つ一つの依存関係に対してオブジェクトを生成する処理と他のオブジェクトに注入するための処理を書かなくてすむようにDIコンテナが存在する。

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

class Movie {
}

interface MovieRepository {
    void save(Movie movie);
}

@Component
@Profile("prod")
class JdbcMovieRepository implements MovieRepository {
    public void save(Movie movie) {
        System.out.println("JDBC: save to DB");
    }
}

@Component
@Profile("mock")
class MockMovieRepository implements MovieRepository {
    public void save(Movie movie) {
        System.out.println("Mock: save (no-op)");
    }
}

@Service
class MovieService {
    private final MovieRepository movieRepository;

    public MovieService(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    public void register(Movie movie) {
        movieRepository.save(movie);
    }
}

@SpringBootApplication
public class SpringDIExample implements CommandLineRunner {
    private final MovieService movieService;

    public SpringDIExample(MovieService movieService) {
        this.movieService = movieService;
    }

    @Override
    public void run(String... args) {
        movieService.register(new Movie());
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(SpringDIExample.class).profiles("prod").run(args);
        new SpringApplicationBuilder(SpringDIExample.class).profiles("mock").run(args);
    }
}

先程とは異なり、依存オブジェクトを生成する部分と注入する部分をDIコンテナが勝手にやってくれるというイメージが伝わればOK。


SpringにおけるDI関連の用語

  • Bean: DIコンテナが管理しているオブジェクト
  • Bean定義: Beanを定義する情報。以下のような手段で記述できる
    • ステレオタイプアノテーションをつける
      • @Component: 汎用的に使われる。
      • @Service: 技術的には@Componentと同じだが、DDDにおけるServiceを表すマーカとして用いる。
      • @Repository: Repositoryの具象クラスにつける。データベース周りの例外をSpringが提供するものに自動的に変換される。
      • @Controller: Controllerの具象クラスにつけることで、Springがコントローラーとして認識する。コントローラーにはRequestMapping系のアノテーションをつけることができ、HTTP Request等を処理できる。
    • @Beanをメソッドにつける: @Beanをつけたメソッドが返すインスタンスをDIコンテナに登録する
    • XMLに<bean>タグをつける: (後方互換性以外で使うことがなさそうなので省略)
  • コンフィグレーション: @Configurationを付けたクラス。@Beanを付けたメソッドをまとめて書くための場所で、外部ライブラリのクラスなど @Component を付けられないものを Bean として登録したいときに使う。(JavaConfig とも呼ばれる)
  • プロファイル: @Profileをつけて、いくつかのコンフィグレーションをグルーピングする仕組み。
  • オートコンフィグレーション: Spring Bootの機能で、@EnableAutoConfiguration(@SpringBootApplicationに含まれる)により、GradleやMavenのファイルに依存関係の設定を使用して@Bean等の設定なしに自動的にDIコンテナに登録する仕組み。
  • コンポーネントスキャン: ステレオタイプアノテーションがついたクラスを検出し、それらをBean定義として登録する仕組み。@ComponentScanはスキャン対象のパッケージを指定するためのアノテーションである。
  • DIコンテナの起動: DIコンテナオブジェクトを作成すること。昔はAnnotationConfigApplicationContext()を直接呼び出していたが、Spring Bootでは@SpringBootApplicationをつけたメインクラスをSpringApplication.run()で起動することで自動的に初期化される。
  • インジェクション: DIコンテナが依存オブジェクトを注入すること
    • Constructor-based Dependency Injection: @Autowiredをつけたコンストラクタの引数に対して依存オブジェクトを注入する。コンストラクタ引数の解決は基本的に型によって行われるが@Qualifier等を使い明示的に解決も可能。フィールドをイミュータブルにできるので推奨される。
    • Setter-based Dependency Injection: @Autowiredをつけたセッターメソッドの引数に対して依存オブジェクトを注入する。
    • フィールドインジェクション: プロになるためのSpring入門では軽く紹介されていたが、Spring 公式ドキュメントでは存在を抹消されていることもあり、本記事では取り扱わない。

@Beanがついたメソッド同士では依存オブジェクトを引数に設定するだけで注入できる。
https://spring.pleiades.io/spring-framework/reference/core/beans/java/bean-annotation.html#beans-java-dependencies

プリミティブ型やStringのような値はDIコンテナで管理せず、@Valueを使って値を注入する。


サンプルアプリでの使用例

どんなアプリか

GitHubリンク

  • JSONを返すだけのREST API
    image.png
  • ユーザ検索機能(DB接続あり)
    image.png
    image.png

DIコンテナの起動

Springアプリケーションを起動する際にDIコンテナも起動する。

@SpringBootApplication は以下の3つのアノテーションを統合したもの。

アノテーション 役割
@Configuration このクラスをBean定義クラスとして登録する
@EnableAutoConfiguration クラスパスを元にBeanを自動設定する
@ComponentScan(デフォルトはメインクラスのパッケージ配下) 指定パッケージ配下のコンポーネントをスキャンする

SpringApplication.run()によってDIコンテナが起動する。

StudyApplication.java
package com.example.study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudyApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudyApplication.class, args);
    }
}

Bean定義

ステレオタイプアノテーションを使ったBean定義

@ControllerをつけることでオブジェクトがDIコンテナにコントローラーとして登録される。
Spring MVCはこれを目印にして、RequestMapping系がついたものをHTTPハンドラーとして認識するため、クライアントからのリクエスト時に呼び出されるようになる。

UserController.java
// 一部抜粋
@Controller
@RequestMapping("/users")
public class UserController {

    private final UserService userService;
    private final UserConverter userConverter;

    public UserController(UserService userService, UserConverter userConverter) {
        this.userService = userService;
        this.userConverter = userConverter;
    }

    @GetMapping
    public String searchForm() {
        return "user/search";
    }

    @GetMapping("/{userId}")
    public String showProfile(@PathVariable String userId, Model model) {
        Optional<User> user = userService.findByUserId(userId);
        if (user.isEmpty()) {
            model.addAttribute("errorMessage", "ユーザーが見つかりませんでした: " + userId);
            return "user/search";
        }
        UserResponse response = userConverter.toResponse(user.get());
        model.addAttribute("user", response);
        return "user/profile";
    }
}

コントローラーから呼び出されるサービスには@Serviceがつけられている。

UserService.java
// 一部抜粋
@Service
public class UserService {

    private final UserRepository userRepository;

    // @Autowired // コンストラクタ1つの場合は省略可能
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public Optional<User> findByUserId(String userId) {
        return userRepository.findByUserId(userId);
    }
}

サービス経由で呼び出されるリポジトリには@Repositoryがつけられている。

UserRepositoryImpl.java
// 一部抜粋
@Repository
public class UserRepositoryImpl implements UserRepository {

    private final UserMapper userMapper;
    private final UserConverter userConverter;

    public UserRepositoryImpl(UserMapper userMapper, UserConverter userConverter) {
        this.userMapper = userMapper;
        this.userConverter = userConverter;
    }

    /**
     * NOTE: MyBatisを差し替え可能なように切り離しいている(依存性逆転の原則)。
     * 依存性逆転とはdomainがInterfaceを定義し、daoがそれを実装することをいう。これによい、domainはdaoに依存しないようになる。
     */
    @Override
    public Optional<User> findByUserId(String userId) {
        return userMapper.findByUserId(userId)
                .map(userConverter::toUser);
    }
}

@Beanを使ったBean定義

JSONを返すだけのREST APIで時刻データを表示しており、その際に使用する依存オブジェクトを@Beanをつけたメソッドで生成している(外部ライブラリのクラスはステレオタイプアノテーションを使ってBean定義できない)。

AppConfig.java
package com.example.study.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration
public class AppConfig {

    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }
}

注入(インジェクション)

@Beanで定義した依存オブジェクトを@Autowiredを使って注入する例

@Autowiredの省略は「コンストラクタが1つの場合に限る(Spring 4.3以降)

HelloController.java
package com.example.study.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.Clock;
import java.time.LocalDateTime;

@RestController
public class HelloController {

    private final Clock clock;

    // @Autowired // NOTE: コンストラクタ1つの場合は省略可能
    public HelloController(Clock clock) {
        this.clock = clock; // 依存オブジェクトがConstructer Injectionで注入された
    }

    record Greeting(String message, String name, LocalDateTime timestamp) {}

    @GetMapping("/hello")
    public Greeting hello(
            @RequestParam(defaultValue = "World") String name) {
        return new Greeting("Hello, %s!".formatted(name), name, LocalDateTime.now(clock));
    }
}

UserService.java
// 再掲
@Service
public class UserService {

    private final UserRepository userRepository;

    // @Autowired // NOTE: コンストラクタ1つの場合は省略可能
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;  // 依存オブジェクトがConstructer Injectionで注入された
    }

    public Optional<User> findByUserId(String userId) {
        return userRepository.findByUserId(userId);
    }
}

Reference

  1. https://martinfowler.com/articles/injection.html 2 3 4

  2. ここでいうAssemblerは別のオブジェクトくらいの意味であり、機械語などの話ではない。1を参照

  3. ここでいうコンテナは仮想化技術ではなく、PicoContainerSpringのようなDIコンテナの話である。1を参照。

  4. https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?