Spring DIの基本的な話ですが、よくはまっている人を見るので書いておきます。
例として次のJavaConfigがある場合を考えてみましょう。
@Configuration
@ComponentScan
public class AppConfig {
@Bean
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// ...
}
パスワードをハッシュ化するアルゴリズムとして、「SHA-256」と「BCrypt」が用意されています。 これらは同じPasswordEncoder
インタフェースで実装されているため、
以下のように@Autowired
でインジェクションしようとすると、
@Component
public class UserServiceImpl implements UserService {
@Autowired
PasswordEncoder passwordEncoder;
// ...
}
NoUniqueBeanDefinitionException
が発生します。
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
を指定してください。
@Component
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("sha256PasswordEncoder")
PasswordEncoder passwordEncoder;
// ...
}
これで、Sha256PasswordEncoder
がインジェクションされるようになります。
また、JavaConfigでのBean定義@org.springframework.context.annotation.Primary
アノテーションをつけると@Qualifier
で 修飾しなかった場合に使用されるBeanを指定できます。
次の例をみましょう。
@Configuration
@ComponentScan
public class AppConfig {
@Bean
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
この場合は、次のように@Qualifier
を指定しない場合はBCryptPasswordEncoder
がインジェクションされます。
@Autowired
PasswordEncoder passwordEncoder;
次のように@Qualifier
でBean名にsha256PasswordEncoder
を指定した場合はSha256PasswordEncoder
がインジェクションされます。
@Autowired
@Qualifier("sha256PasswordEncoder")
PasswordEncoder passwordEncoder;
ただし、@Qualifier
で指定する名前に実装の名前が含まれるのは好ましくありません。 DIによって疎結合したにも関わらず、呼び出し側で実装を特定してしまうとDIの意味がありません。ハリウッドの原則("Don't call us, we'll call you.")に明らかに反していますね。
文字列で実装を特定している分、DIを使わない場合よりも悪い状況とも言えます。
このような場合、Bean名を"実装名"ではなく"用途名"にするのが良いです。先の例では、 「デフォルトでは強力なBCryptアルゴリズムを使いたいが、軽量なSHA-256アルゴリズムも用意したい」 とします。この場合、次のようのようにSha256PasswordEncoder
のBean名に用途を示すlightweight
を指定するのがより良いです。
@Configuration
@ComponentScan
public class AppConfig {
@Bean(name = "lightweight")
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
"軽量なアルゴリズム"を使用した実装をインジェクションしたい場合は、 次のように@Qualifier
を指定すれば良いです。
@Autowired
@Qualifier("lightweight")
PasswordEncoder passwordEncoder;
より良い方法として、"用途"は文字列ではなく型(アノテーション)で表現することもできます。
次のように@Qualifier
を付与した@Lightweight
アノテーションを作成しましょう。
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
アノテーションを付与してください。
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Lightweight
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// ...
}
インジェクションする際も@Lightweight
を付与すれば良いです。
@Autowired
@Lightweight
PasswordEncoder passwordEncoder;
型安全であるため、この方法がBean定義の重複に対する最も良い方法だと考えられます。 もちろん@Sha256
のように実装を直接示すアノテーションを作るのは良くありません。
Spring CloudでRestTemplate
にクライアントロードバランサを含めるかどうかの判断にもこの方法が利用されています。
@Autowired
@LoadBalanced // ロードバランサ有り
RestTeamplate restTemplate;
実装にはRibbonが使われているのですが、修飾名には実装名を含めないのがポイントです。