Spring FrameworkとDIについて

Oct 3, 2016

DIのモチベーション

※ 参考: Spring徹底入門 CHAPTER2

一定規模以上のアプリケーションを開発するとき、各モジュールごとに実装を進めていくやり方が取られる。

例えば以下のようなインターフェースが必要になる場合。

  • UserService
  • UserRepository
  • PasswordEncoder

UserServiceインターフェースを実装するUserServiceImplというクラスを実装する場合、

class UserServiceImpl implements UserService {
  public UserServiceImpl(DataSource dataSource) {
    this.userRepository = new UserRepositoryImpl(dataSource);
    // ... 省略
  }
}

上記のようなコードであると、UserServiceImplは依存するUserRepositoryImplが完成していないと実装ができない。 これは結合度の高いクラスと言える。

以下のようにコンストラクタに依存するコンポーネントを渡す書き方もできる。

public UserServiceImpl(UserRepositoryImpl userRepository) {
  this.userRepository = userRepository;
  // ... 省略
}

しかし、この場合もUserRepositoryImplという依存関係を渡していることになる。 依存関係の注入をしている箇所はどちらにしても発生しているので、依存関係自体が変わったとき(たとえは、UserRepositoryImpl内でUserRepositoryImplが必要なくなった場合)には、コンストラクタ自体のインターフェースを変える必要がありコストが大きい。

このような依存性の注入を行う機能として「DIコンテナ」を別に用意し、依存関係の注入(dependency injection)はこのDIコンテナにまかせる、というのがSpringのDIコンテナの考え方となる。

あるインスタンスAを利用したいインスタンスが複数存在する場合、

  • DIコンテナは、様々なインスタンスをコンポーネントとして管理する
  • 依存するコンポーネントを必要とするインスタンスはDIコンテナを介してコンポーネントを取得する

このようにインスタンスをDIコンテナに管理させることにより、以下のようなメリットがある:

  • インスタンスのスコープを制御できる
  • インスタンスのライフサイクルを制御できる
    • 上記2点の制御について。シングルトンなのか、プロトタイプ(都度生成)なのか、Webアプリケーションのセッションごとなのか、管理方法はDIの仕事にする
    • こうすることによって、依存インスタンスを使う側のインスタンスに余計な仕事をさせなくてもすむ
  • 共通機能を埋め込める
  • コンポーネント間が疎結合となり、単体テストを行いやすくなる

SpringのDI

SpringのDIコンテナはApplicationContextというクラスによって実現されている。 以下の例のようにして、DIコンテナからコンポーネント(SpringではBeanと呼ぶ)を取り出すことができる。

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);

AppConfig.classはSpringのDIコンテナの設定ファイルとなるクラス。

class AppConfig {
  @Bean
  UserService userService() {
    return new UserServiceImpl();
  }
}
  • DIコンテナは設定ファイル(上記例だとAppConfig.class)をもとにDIコンテナにコンポーネント(Bean)を登録する
  • アプリケーションは、ApplicationContextインターフェースを通じて、DIコンテナに登録されたコンポーネントを取得する

コンポーネントの定義方法

Springのコンポーネント定義には以下のやり方がある:

  • JavaベースConfiguration
    • @Configurationアノテーションを付与したクラスに、@Beanアノテーションを付与したメソッドを定義してコンポーネントを定義する
  • XMLベースConfiguration
    • XMLファイル内にbean定義を書く
  • アノテーションベースのConfiguration
    • @Componentアノテーションが付与されたクラスを「コンポーネントスキャン」という機能を使って自動的にDIコンテナに登録する
    • Spring Bootの @SpringBootApplication というアノテーションをメインクラスに付与すると、@Configuration @EnableAutoConfiguration @ComponentScanが有効になる。
    • @Componentscanと同様に、スキャンするpackageの指定も可能(SpringBootApplication (Spring Boot Docs 1.4.1.RELEASE API)

デフォルトでは、登録されるBeanの名前はクラスの先頭の大文字を小文字にしたものになる。

e.g. @ComponentアノテーションでBean登録したUserServiceクラスの場合はuserServiceがbean名になる。

@Component("myUserService")のように、明示的にbean名を指定することも可能。

コンポーネントの注入方法

Springではコンポーネントを注入(injection)する方法をいくつか提供している。

セッターインジェクション

componentがsetterを持つ場合、そのsetterの引数に対して依存するcomponentを注入する。

@Bean
UserService userService() {
  UserServiceImple userService = new UserServiceImpl();
  userService.setUserRepository(userRepository);
  return userService;
}

以下のように登録済みのコンポーネントを引数として取ることもできる。

@Bean
UserService userService(UserServiceImple userServiceImpl, UserRepository userRepository) {
  userServiceImpl.setUserRepository(userRepository);
  return userServiceImpl;
}

コンストラクタインジェクション

injectionは、@Component を付与したクラスのメソッドに、 @Autowired annotationを使って設定することもできる。

@Component
public class UserServiceImpl implements UserService {
  private UserRepository userRepository;

  public UserServiceImpl() {}

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

@Component クラスにコンストラクタが1つしかない場合は、自動で @Autowired 扱いとなり、以下のようにDIできる。

@Component
public class UserServiceImpl implements UserService {
  @Data // lombok
  private final UserRepository userRepository;

  // コンストラクタが1つしかない場合は自動的にAutowiredとなる
  public UserServiceImpl(UserRepository userRepository) {
    this.userRepository = userRepository;
  }
}

コンストラクタインジェクションのメリットは、フィールドにfinalをつけて不変にできること、とのこと。

これは他のインジェクション方法では実現できない。

フィールドインジェクション

フィールドインジェクションは最もコード量を少なくできるインジェクション方法となる。

@Component
public class UserServiceImpl implements UserService {
  @Autowired
  UserRepository userRepository; // package local
}

ただし、汎用ライブラリの中でこのインジェクション方法を使ってしまうと、Spring以外のDIコンテナで使い回しできなくなる。

Autowiring

@Autowired アノテーションのAutowiring機能を使うと、 @Bean 設定をいちいち書かなくてもDIすることができる。 Autowiringには以下の名前の解決方法がある。

型による解決方法

前項で例にあげた @Autowired によるインジェクションは、「型による解決方法」となる。

これは

  • セッターインジェクション
  • コンストラクタインジェクション
  • フィールドインジェクション

いずれのインジェクション方法でも利用可能。 型によるAutowiringは、デフォルトではインジェクションされることが必須となっており、@Autowired で指定されている型のBeanが1つも登録されていないと NoSuchBeanDefinitionException が発生する。

インジェクションを必要としない場合は、以下のように required=false を指定することで例外の発生を回避することができる。

@Autowired(required=null)
UserService userService;

Spring4からは、以下のようにOptional型を使うことができる。

@Autowired
Optional<UserService> userService = Optional.nullable(userService);

// TODO: こんな感じ?かなり適用なコード…
public void createUser(String username) {
  userService.ifPresent(userService -> userService.create(username));
}
複数の同一型のBeanがある場合

型によるAutowiringの場合、インジェクション対象の型が複数DIコンテナにBean定義されていると、SpringはどのBeanを使ってよいかわからない。 そのため、 NoUniqueBeanDefinitionException が発生する。

同じ型のBeanが複数定義されている場合は、それぞれのBean定義に @Qualifier アノテーションでBeanごとに固有名を指定する必要がある。

// Config
@Config
public class MyConfig {

    @Bean(name = "notificationEmailInfoTemplate")
    public RabbitTemplate notificationEmailInfoTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());

        String exchangeName = property.getBindings().get("notification-email-info").getExchange().getName();
        rabbitTemplate.setExchange(exchangeName);
        String queueName = property.getBindings().get("notification-email-info").getQueue().getName();
        rabbitTemplate.setQueue(queueName);

        return rabbitTemplate;
    }
}
// AutowireするComponent
@Component
public class MyClass {

  @Autowired
  @Qualifier("notificationEmailInfoTemplate")
   RabbitTemplate notificationEmailInfoTemplate;
}

名前による解決方法

Bean名が

  • フィールド名
  • プロパティ名

と一致するBeanをインジェクションする機能もある。 上述した @Qualifier アノテーションのようにして、 @Resource アノテーションのnameにBean名を指定することができる。

@Resource(name = "notificationEmailInfoTemplate")
RabbitTemplate notificationEmailInfoTemplate;

// name=を省略すると、Bean名がフィールド名と一致するときにインジェクション候補となる
// @Resource
// RabbitTemplate notificationEmailInfoTemplate;

コレクション型のBeanのAutowiring

コレクション型もAutowiringすることができる。

@Component
public class Class1 implements MyClass {}
@Component
public class Class2 implements MyClass {}
@Component
public class Class3 implements MyClass {}

@Autowired
public List<MyClass> lists; // MyClassインターフェースのBeanのList
@Autowired
public Map<String, MyClass> maps; // keyがBean名、valueがBeanのMap
@Autowired
public List<Class1> lists; // Class1のBeanのみのList

参考文献

Retrun to top