📝 BLOG.IK.AM

@making's memo
(🗃 Categories 🏷 Tags)

SpringのBean定義(Java Config)で型が重複する場合のインジェクション方法

🗃 {Programming/Java/org/springframework/core}

🏷 Java 🏷 Spring

🗓 Updated at 2016-03-09T01:57:53+09:00 by Toshiaki Maki  🗓 Created at 2016-03-09T01:54:01+09:00 by making  {✒️️ Edit  ⏰ History}


🚨 Warning: This content is old. Please don't trust too much.

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を使用したい場合は、以下のように@Qualifiersha256PasswordEncoderを指定してください。

@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が使われているのですが、修飾名には実装名を含めないのがポイントです。