SpringのConfigurationPropertiesのアノテーションを付与したBeanを複数登録する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。齋藤です。

今回は久しぶりにSpringです。 色々リファクタリングをしている時にふとSpringのソースを覗いてみたところ 楽しそうな内容が書けそうだったので書きました。

今回の記事は次のようなケースで使えるでしょう。

  • ConfigurationPropertiesのクラスをBeanとして複数登録させたい
  • ConfigurationPropertiesのクラスに@Componentを付与したくない
  • ConfigurationPropertiesのクラスのprefixを利用場所で変えたい

はじめに

今、自分たちがやっているプロジェクトでは次のようなクラスを作ることがあります。 application.properties を型安全に取り扱う仕組みですね。

@ConfigurationProperties("config.hoge")
@Component
public class HogeConfig {
  String name;
  String test;
  // getter, setter, equals, hashCode 省略
}   

先ほども書きましたが、次のようなことをしたいケースがあるかもしれません。(ないかもしれません。いや、多分ない。)

  • ConfigurationPropertiesのクラスをBeanとして複数登録させたい
  • ConfigurationPropertiesのクラスに@Componentを付与したくない
  • ConfigurationPropertiesのクラスのprefixを利用場所で変えたい

今回の記事では、次のような SampleConfig クラスを例に記事を書いていきます。 先ほど挙げたコードから、@Component@ConfigurationProperties のアノテーションをほぼ外しただけです。

public class SampleConfig {
  String name;
  String test;
  // getter, setter, equals, hashCode 省略
}   

どうやって複数のprefixでBeanを登録するのか

では、やっていきますが ConfigurationPropertiesのクラスを普通にBean登録するだけです。 これだけです。

@Configuration
public class SampleConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "config.hoge")
    SampleConfig configHoge() {
        return new SampleConfig();
    }

    @Bean
    @ConfigurationProperties(prefix = "config.fuga")
    SampleConfig configFuga() {
        return new SampleConfig();
    }
}

多分、パッとみたら大体わかるでしょう。

config.hoge配下の設定を読み取るconfigHoge というBeanと config.fuga配下の設定を読み取るconfigFuga というBeanが登録されています。 もちろん、application.properties などに書かれた設定が読み込まれます。

なぜ先ほどのコードが動くのか

その秘密は ConfigurationPropertiesBindingPostProcessor にあります。

Springにはそもそも、BeanPostProcessorという仕組みがあり Beanの登録時に何かをやりたい仕組みが存在します。

BeanPostProcessorは次のようなインターフェースです。 beanとそのbeanNameが渡されたら、beanを戻り値とするようなメソッドを持つインターフェースです。

public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

ConfigurationPropertiesアノテーションに関わるBeanPostProcessorは ConfigurationPropertiesBindingPostProcessor です。 org.springframework.boot.context.properties パッケージに所属しています。

さて、ここで、先ほどの ConfigurationPropertiesBindingPostProcessorpostProcessBeforeInitialization を覗いてみます。 次のようなコードが書かれています。

    // 省略
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        // クラスからアノテーション探す
        ConfigurationProperties annotation = AnnotationUtils
                .findAnnotation(bean.getClass(), ConfigurationProperties.class);
        if (annotation != null) {
            postProcessBeforeInitialization(bean, beanName, annotation);
        }
        // ファクトリメソッドからアノテーションを探す ⇨ この記事ではConfigurationクラスのメソッド
        annotation = this.beans.findFactoryAnnotation(beanName,
                ConfigurationProperties.class);
        if (annotation != null) {
            postProcessBeforeInitialization(bean, beanName, annotation);
        }
        return bean;
    }
    // 省略

なるほど、なんとなくわかってきたでしょうか。 クラスそのものにアノテーションが付いているか、もしくはファクトリメソッドに ConfigurationProperties アノテーションが付いていれば postProcessBeforeInitialization メソッドが呼ばれるようになっています。

postProcessBeforeInitialization メソッドを見ると ConversionServiceだったり、Validatorだったりの単語が踊っています。 今回は解説しませんが、このメソッドの中で application.properties などから読み込んだ設定値を ConfigurationPropertiesのアノテーションが付与されたBeanに値をセットしています。

[追記] DIして使う際には注意

簡単にですが、テストコードで使う場合の注意点を示しておきます。 ConfigurationPropertiesもただのBeanですので、 Dependency Injectionする場合には注意が必要です。

具体的には以下のようなコードなら動作します。(テストコードから抜粋) 以下のように、QualifierでBean名を指定したりする必要があります。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ConfigpropsApplicationTests {

    @Autowired
    @Qualifier("configHoge")
    SampleConfig configHoge;

    @Autowired
    @Qualifier("configFuga")
    SampleConfig configFuga;

}

もしくは次のようなCustom Qualifier Annotationを使って使って見るのも良いでしょう。

// Hogeアノテーション
@Target({
    ElementType.METHOD,
    ElementType.FIELD,
    ElementType.TYPE,
    ElementType.TYPE_PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Hoge {    
}

// Fugaアノテーション
@Target({
    ElementType.METHOD,
    ElementType.FIELD,
    ElementType.TYPE,
    ElementType.TYPE_PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fuga {
}

// Configurationクラス
@Configuration
public class QualifierSampleConfiguration {

    @Bean
    @Hoge
    @ConfigurationProperties("config.hoge")
    SampleConfig qualifierConfigHoge() {
        return new SampleConfig();
    }

    @Bean
    @Fuga
    @ConfigurationProperties("config.fuga")
    SampleConfig qualifierConfigFuga() {
        return new SampleConfig();
    }

    @Bean
    @ConfigurationProperties("config.default")
    @Primary // PrimaryアノテーションでデフォルトのBeanを設定
    SampleConfig config() {
        return new SampleConfig();
    }

}

// 利用の仕方
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConfigpropsApplicationTests {
    @Autowired
    SampleConfig defaultConfig;

    @Autowired
    @Fuga
    SampleConfig qualifierConfigHoge;

    @Autowired
    @Hoge
    SampleConfig qualifierConfigFuga;
}

まとめ

いかがだったでしょうか。今回は ConfigurationProperties アノテーションの混み入った使い方を紹介しました。 今回の記事で追った内容を踏まえると、ConfigurationPropertiesもただのSpringで言うところのBeanといったところでしょうか。

機会があれば有効活用してみてください!

使う際には一癖ありますが、ハマれば強力かもしれません。

今回書いたコードはGitHubに置いてあります