--- title: SpringのBean定義(Java Config)で型が重複する場合のインジェクション方法 tags: ["Java", "Spring"] categories: ["Programming", "Java", "org", "springframework", "core"] date: 2016-03-09T01:54:01Z updated: 2016-03-09T01:57:53Z --- Spring DIの基本的な話ですが、よくはまっている人を見るので書いておきます。 例として次のJavaConfigがある場合を考えてみましょう。 ``` java @Configuration @ComponentScan public class AppConfig { @Bean PasswordEncoder sha256PasswordEncoder() { return new Sha256PasswordEncoder(); } @Bean PasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } // ... } ``` パスワードをハッシュ化するアルゴリズムとして、「SHA-256」と「BCrypt」が用意されています。 これらは同じ`PasswordEncoder`インタフェースで実装されているため、 以下のように`@Autowired`でインジェクションしようとすると、 ``` java @Component public class UserServiceImpl implements UserService { @Autowired PasswordEncoder passwordEncoder; // ... } ``` `NoUniqueBeanDefinitionException`が発生します。 ``` console com.example.UserServiceImpl.passwordEncoder; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.example.PasswordEncoder] is defined: expected single matching bean but found 2: bcryptPasswordEncoder,sha256PasswordEncoder ``` 候補が複数あるため、使用する際は使用したいBeanの名前を明示しなくてはいけません。 SHA-256を使用したい場合は、以下のように`@Qualifier`に`sha256PasswordEncoder`を指定してください。 ``` java @Component public class UserServiceImpl implements UserService { @Autowired @Qualifier("sha256PasswordEncoder") PasswordEncoder passwordEncoder; // ... } ``` これで、`Sha256PasswordEncoder`がインジェクションされるようになります。 また、JavaConfigでのBean定義`@org.springframework.context.annotation.Primary`アノテーションをつけると`@Qualifier`で 修飾しなかった場合に使用されるBeanを指定できます。 次の例をみましょう。 ``` java @Configuration @ComponentScan public class AppConfig { @Bean PasswordEncoder sha256PasswordEncoder() { return new Sha256PasswordEncoder(); } @Bean @Primary PasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` この場合は、次のように`@Qualifier`を指定しない場合は`BCryptPasswordEncoder`がインジェクションされます。 ``` java @Autowired PasswordEncoder passwordEncoder; ``` 次のように`@Qualifier`でBean名に`sha256PasswordEncoder`を指定した場合は`Sha256PasswordEncoder`がインジェクションされます。 ``` java @Autowired @Qualifier("sha256PasswordEncoder") PasswordEncoder passwordEncoder; ``` ただし、`@Qualifier`で指定する名前に実装の名前が含まれるのは好ましくありません。 DIによって疎結合したにも関わらず、呼び出し側で実装を特定してしまうとDIの意味がありません。ハリウッドの原則("Don't call us, we'll call you.")に明らかに反していますね。 文字列で実装を特定している分、DIを使わない場合よりも悪い状況とも言えます。 このような場合、Bean名を"実装名"ではなく"用途名"にするのが良いです。先の例では、 「デフォルトでは強力なBCryptアルゴリズムを使いたいが、軽量なSHA-256アルゴリズムも用意したい」 とします。この場合、次のようのように`Sha256PasswordEncoder`のBean名に用途を示す`lightweight`を指定するのがより良いです。 ``` java @Configuration @ComponentScan public class AppConfig { @Bean(name = "lightweight") PasswordEncoder sha256PasswordEncoder() { return new Sha256PasswordEncoder(); } @Bean @Primary PasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` "軽量なアルゴリズム"を使用した実装をインジェクションしたい場合は、 次のように`@Qualifier`を指定すれば良いです。 ``` java @Autowired @Qualifier("lightweight") PasswordEncoder passwordEncoder; ``` より良い方法として、"用途"は文字列ではなく型(アノテーション)で表現することもできます。 次のように`@Qualifier`を付与した`@Lightweight`アノテーションを作成しましょう。 ``` java import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface Lightweight { } ``` Java Configで"軽量な"アルゴリズムに対応する実装の定義に`@Lightweight`アノテーションを付与してください。 ``` java @Configuration @ComponentScan public class AppConfig { @Bean @Lightweight PasswordEncoder sha256PasswordEncoder() { return new Sha256PasswordEncoder(); } @Bean @Primary PasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } // ... } ``` インジェクションする際も`@Lightweight`を付与すれば良いです。 ``` java @Autowired @Lightweight PasswordEncoder passwordEncoder; ``` 型安全であるため、この方法がBean定義の重複に対する最も良い方法だと考えられます。 もちろん`@Sha256`のように実装を直接示すアノテーションを作るのは良くありません。 Spring Cloudで`RestTemplate`にクライアントロードバランサを含めるかどうかの判断にも[この方法が利用されています](https://github.com/spring-cloud/spring-cloud-commons/blob/master/docs/src/main/asciidoc/spring-cloud-commons.adoc#spring-resttemplate-as-a-load-balancer-client)。 ``` java @Autowired @LoadBalanced // ロードバランサ有り RestTeamplate restTemplate; ``` 実装にはRibbonが使われているのですが、修飾名には実装名を含めないのがポイントです。