DI(依存性の注入)


DI(依存性の注入)は、コンポーネントを構成するインスタンスの生成と依存関係の解決をソースコードから切り離すことができます。

出典:Spring徹底入門

DIコンテナを経由したインスタンス管理には次のメリットがあります。

  • インスタンスのスコープを制御できる
  • インスタンスのライフサイクルを制御できる
  • 共通機能を組み込める
  • コンポーネント間画素結合になるため、ユニットテストがしやすい

🍮 DIに関する用語

Spring FrameworkではDIコンテナに登録するコンポーネントのことを「Bean」といいます。
また、DIコンテナからBeanを取得することを「ルックアップ」といいます。

🎂 インジェクションの種類

Spring Frameworkでは次の3つのインジェクション方法をサポートしています。

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

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

ここではコンストラクタインジェクションをアノテーションベースで行う方法を紹介します。

Bean定義用のアノテーションが付与されたクラスをスキャンしてDIコンテナに登録します。
自動注入のことを「オートワイヤリング」といいます。

@Component // アノテーションを付与してコンポーネントスキャンの対象にする
public class UserRepositoryImpl implement UserRepository {
}
@Component // アノテーションを付与してコンポーネントスキャンの対象にする
public class BCryptPasswordEncoder implement PasswordEncoder {
}
@Component // アノテーションを付与してコンポーネントスキャンの対象にする
public class UserServiceImpl implement UserService {
@Autowired // 対象の型が一致するBeanをDIコンテナから探してインジェクションする(オートワイヤリング)
public UserServiceImpl(UserRepository UserRepository, PasswordEncoder PasswordEncoder) {
}
}

コンポーネントスキャンを有効にするにはBean定義ファイルに設定を記述します。

@Configuration
@ComponentScan(com.example.demo) //コンポーネントスキャンの有効化、パッケージ名を引数に
public class AppConfig {
}

Beanの名前はクラス名の先頭を小文字にしたものです。UserRepositoryImplならuserRepositoryImplです。

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

セッターの引数に対して、依存するコンポーネントを注入できます。

@Component
public class UserServiceImpl implements UserService {
private UserRepository UserRepository;
private PasswordEncoder passwordEncoder;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
}

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

インジェクトしたいフィールドに@AutowiredをアノテートすることでDIを実現できます。

@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository UserRepository;
@Autowired
private PasswordEncoder passwordEncoder;
}

フィールドインジェクション対象のフィールドの可読性(アクセス制御)は、指定なし(パッケージプライベート)がよいです。
ユニットテストでテスト用のフィールドを直接設定できます。

@Test
public void testCreate() throws Exception {
UserServiceImpl userService = new UserServiceImpl();
userService.userRepository = new DummyUserRepository();
userService.passwordEncoder = new DummyPasswordEncoder();
}

🚜 オートワイヤリング

オートワイヤリングには「型によるオートワイヤリング」と「名前によるオートワイヤリング」の2つがあります。

型によるオートワイヤリング

型によるオートワイヤリングはすべのインジェクションの形式で利用できます。

オートワイヤリングするBeanがない

インジェクション対象の型をもつBeanがない場合はorg.springframework.beans.factory.NoSuchBeanDefinitionExceptionが発生します。@Autowired(require=false)とすれば、例外発生を回避できます。フィールド値はnullになります。

@Autowired(require=false) // 対象のBeanがなくても例外は発生しない。nullが入る
private UserRepository UserRepository;
@Autowired // Optionalをつかってもrequire=falseと同義になる
Optional<PasswordEncoder> passwordEncoder;

オートワイヤリングするBeanが複数ある

インジェクション対象の型がDIコンテナに複数定義されている場合は、org.springframework.beans.factory.NoUniqueBeanDefinitionExceptionが発生します。
@QualifierアノテーションでBean名を指定してください。

@Autowired
@Qualifier(lightweight) // 用途名をBean側につけてそれを引数で指定します
PasswordEncoder passwordEncoder;

もしくはBean定義に@Primaryアノテーションをつけると、複数ある場合に優先して利用されます。

@Bean
@Primary // 複数ある場合は @PrimaryのつくBeanが優先
PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

名前によるオートワイヤリング

名前によるオートワイヤリングは「セッターインジェクション」と「フィールドインジェクション」で利用できます。
@Resourceを使ってBean名がフィールド名またはプロパティ名と一致するBeanをインジェクトします。

@Resource
PasswordEncoder passwordEncoder;

🐮 コレクションやマップ型のオートワイヤリング

public Inteface IF<T> {}
@Component
public class IntIF1 implements IF<Integer> {}
@Component
public class IntIF2 implements IF<Integer> {}
@Component
public class StringIF implements IF<String> {}

この定義を使うと、次のようにオートワイヤリングできます。

@Autowired
List<IF> ifList; //=> IntIF1, IntIF2, StringIF
@Autowired
List<IF<Integer>> ifList2; //=> IntIF1, IntIF2
@Autowired
Map<String, IF> ifMap; //=> { intIF1 = IntIF1のBean, intIF2 = IntIF2のBean, stringIF = StringIFのBean }
@Autowired
Map<String, IF<Integer>> ifMap2; //=> { intIF1 = IntIF1のBean, intIF2 = IntIF2のBean }

🍣 コンポーネントスキャン

コンポーネントスキャンはクラスローダーをスキャンして特定のクラスをDIコンテナに追加する手法です。
スキャン対象には次のようなアノテーションを追加します。

アノテーション 説明
@Controller Controllerの役割を担うコンポーネントを示すアノテーション
@Service ビジネスロジックを提供するコンポーネントを示すアノテーション
@Repository データの永続化に関わるコンポーネントを示すアノテーション(O/Rマッパ)
@Component 上記の3つに当てはまらないコンポーネント

🍄 Beanのスコープ

DIのメリットは、Beanの生存期間(スコープ)の管理をコンテナに任せることができる点です。スコープはScopeアノテーションで設定します。

スコープ 説明
singleton DIコンテナの起動時にBeanの新スタンスを生成しSingletonとして扱う。デフォルト値
prototype Beanの取得時に毎回インスタンスを生成。スレットアンセーフなBeanの場合に用いる
session HTTPのセッション単位でBeanインスタンスを生成
request HTTPのリクエスト単位でBeanインスタンスを生成
application サーブレットのコンテキスト単位でBeanのインスタンスを生成

下図はスコープとBeanのインスタンスの関係をsingletonprotypeスコープの例です。

出典:Spring徹底入門

ルックアップメソッドインジェクション

Singletonのインスタンス内でprotypeスコープのインスタンスを単純にDIコンテナで使うと、Singletonが優先されます。
この解決策のひとつにLookupアノテーションを付与する方法があります。

@Componentpublic class UserServiceImpl implements UserService {
public void register(User user, String rawPassword) {
PasswordEncoder passwordEncoder = passwordEncoder();
// ...
}
@Lookup
PassordEncoder passwordEncoder() {
return null; //オーバーライドされるので、戻り値はダミーでOK
}
}

Spring FrameworkがUserServiceImplをオーバーライドしたメソッドを作ってインスタンスを取得します。

Scoped Proxy

異なるスコープの問題を解決するもうひとつの方法にScoped Proxyがあります。

@Component
@Scope(value=request, proxyMode=ScopedProxyMode.INTERFACES)
public class ThreadUnsafePasswordEncode implements PasswordEncoder { /* ... */ }

ルックアップとScoped Proxyの使い分け

  • Scoped Proxy:requestsessionglobalSessionスコープで利用
  • ルックアップメソッドインジェクション:prototypeスコープで利用

😀 Beanのライフサイクル

DIコンテナで管理するBeanのライフサイクルは「初期化フェーズ、利用フェーズ、終了フェーズ」の3つのフェーズで構成されます。
初期化フェーズと終了フェーズについて簡単に説明します。

初期化フェーズ

次のように処理を行います。

出典:Spring徹底入門

たとえば「Post Construct」を活用してキャッシュを作成できます。

@Component
public class UserServiceImpl implements UserService {
@PostConstruct
void generateCache() { /* キャッシュ作成 */ }
}

終了フェーズ

DIコンテナが破棄されるタイミングで「Pre Destroy」の処理を行います。さきほどのキャッシュを削除してみます。

@Component
public class UserServiceImpl implements UserService {
@PreDestroy
void clearCache() { /* キャッシュ破棄 */ }
}

🏀 Configurationの分割

@ImportでConfigurationクラスを指定してインポートできます。

@Configuration
@Import({InfrastructureConfig.class}) // InfrastructureConfig をインポート
public class AppConfig { /* 別クラスの設定がインポート */ }

🏈 環境毎にConfigurationファイルを分ける

@Profileで開発環境と本番環境でConfigurationファイルを分けることができます。

@Configuration
@Profile(development)
public class DevelopmentConfig { /* 開発環境の設定 */ }

どのプロファイルを選択するかは起動時のオプションの-Dspring.profile.activeで指定します。

-Dspring.profiles.active=production

指定がない場合は、spring.profiles.defaultで指定できます。

🐝 参考リンク

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍