Edited at

PropertyOverrideConfigurerを使用して外部設定のプロパティ値をBeanへ設定(上書き)する

Spring Bootには「Type-safe Configuration Properties」という仕組みがあり、外部設定のプロパティ値(プロパティファイル、システムプロパティ、環境変数などに設定した値)を「@ConfigurationPropertiesを付与したBean」と「@ConfigurationPropertiesを付与して生成したBean」のプロパティに設定することができます。


@ConfigurationPropertiesを付与したBean例

@Component

@ConfigurationProperties("demo")
public class DemoProperties {
private String foo;
// ...
}


@ConfigurationPropertiesを付与して生成したBean例

@ConfigurationProperties("demo")

@Bean
DemoProperties demoProperties() {
return new DemoProperties();
}


プロパティ値の指定例

demo.foo=bar


本エントリーでは、Spring Framework本体が提供しているPropertyOverrideConfigurerを使うことで、これと同じようなことができるよ!ということを紹介したいと思います。

Spring Bootは使っているけどSpring Bootの仕組みで手が出せないBeanのプロパティに任意の値を設定したい場合や、諸事情でSpring Bootを使っていない(使えない)けど・・・「Type-safe Configuration Properties」チックなことを実現したいな〜という場合には、PropertyOverrideConfigurerの利用をを検討してみるとよいと思います。個人的には「Type-safe Configuration Properties」はSpring Framework本体で対応してほしいんですけどね・・・(yaml使いたいし・・)。


検証バージョン


  • Spring Boot 2.0.3.RELEASE

  • Spring Framework 5.0.7.RELEASE


PropertyOverrideConfigurerって何者?

PropertyOverrideConfigurerは、SpringのDIコンテナ上で管理するBeanのプロパティをプロパティファイルなどから読み込んだプロパティ値で上書きしてくれる機能です。例えば以下のように、Bean定義ファイル内で設定値をハードコーディングしていたとしても・・・

@Bean

DemoProperties demoProperties() {
DemoProperties properties = new DemoProperties();
properties.setFoo("hoge");
return properties;
}

PropertyOverrideConfigurerのBean定義とプロパティファイルを用意すれば設定値を変えることができます。


PropertyOverrideConfigurerのBean定義例

@Bean

static PropertyOverrideConfigurer propertyOverrideConfigurer(@Value("classpath*:demo-properties.properties") Resource... resources) {
PropertyOverrideConfigurer configurer = new PropertyOverrideConfigurer();
configurer.setLocations(resources);
return configurer;
}


プロパティ値の指定例(src/main/resources/demo-properties.properties)

demoProperties.foo=bar



プロパティキーのルール

PropertyOverrideConfigurerを使用してプロパティ値をBeanへ上書きするためには、指定したプロパティファイルに「{Bean名}.{Beanのプロパティへのパス}」というキーで値を指定する必要があります。もちろんネストしたオブジェクト、MapListなどにも対応しています。

public class DemoProperties {

private String foo;
private final Connection defaultConnection = new Connection();
private final Map<String, Connection> connections = LazyMap.lazyMap(new HashMap<>(), Connection::new);
private final List<Template> templates = LazyList.lazyList(new ArrayList<>(), Template::new);

// ...

static class Connection {
private String host;
private int port;
// ...
}

static class Template {
private Resource file;
private Charset encoding;
// ...
}

}


ネストしたオブジェクトに値を設定する際のプロパティ定義例

demoProperties.defaultConnection.host=localhost

demoProperties.defaultConnection.port=9999


Mapに値を設定する際のプロパティ定義例

demoProperties.connections[github].host=github.com

demoProperties.connections[github].port=443


Listに値を設定する際のプロパティ定義例

demoProperties.templates[0].file=a.txt

demoProperties.templates[0].encoding=UTF-8
demoProperties.templates[1].file=b.txt
demoProperties.templates[1].encoding=Windows-31J


無効なプロパティキーがある場合の動作

プロパティキーの「Bean名」部分に一致するBeanがDIコンテナに存在しない場合、無効なプロパティキーとして扱われデフォルトの動作ではエラーになります。

# ...

# DIコンテナにtestBeanというBeanがいないとエラーになる
testBean.foo=aaa

この動作は、以下のようなBean定義にすることで変更することができます。

@Bean

static PropertyOverrideConfigurer propertyOverrideConfigurer(@Value("classpath*:demo-properties.properties") Resource... resources) {
PropertyOverrideConfigurer configurer = new PropertyOverrideConfigurer();
configurer.setLocations(resources);
configurer.setIgnoreInvalidKeys(true); // trueを設定すると無効なプロパティキーがあってもエラーにならない
return configurer;
}


指定したプロパティファイルが存在しない場合の動作

指定したプロパティファイルが存在しない場合、デフォルトの動作ではエラーになります。この動作は、以下のようなBean定義にすることで変更することができます。

@Bean

static PropertyOverrideConfigurer propertyOverrideConfigurer() {
PropertyOverrideConfigurer configurer = new PropertyOverrideConfigurer();
configurer.setLocations(new ClassPathResource("demo-properties.properties"));
configurer.setIgnoreInvalidKeys(true);
configurer.setIgnoreResourceNotFound(true); // trueを設定するとプロパティファイルがなくてもエラーにならない
return configurer;
}


プロパティプレースホルダの利用

プロパティファイルの指定する設定値には、プロパティプレースホルダ(${...})を指定することができます。プロパティプレースホルダを使用すると、Spring(Spring Boot)プロパティ管理機能で管理しているプロパティ値(環境変数、システムプロパティなどに指定した値)に置き換えることができるため、実行環境によって設定値を変えるような場合に使える仕組みです。

例えば・・・Spring Bootを利用しているアプリケーションなら、


src/main/resources/demo-properties.properties

demoProperties.defaultConnection.host=${default.host:localhost}



src/main/resources/application.properties

default.host=myhost


とすると、実行時には以下のように解釈されmyhostが設定されます。

仮にプレースホルダに指定したプロパティキーが存在が存在しない場合でも、デフォルト値を指定しておけばエラーにはならずにlocalhostが適用されるため、デフォルト値にはローカルの開発環境向けの設定値を指定しておくとよいでしょう。


src/main/resources/application.properties

demoProperties.defaultConnection.host=myhost


ただし、PropertyOverrideConfigurerに指定したプロパティファイルの中で定義しているプロパティキーは、プロパティプレースホルダに指定することはできません。これは、PropertyOverrideConfigurerに指定したプロパティファイルが、Spring(Spring Boot)のプロパティ管理機能で管理するプロパティ値として扱われないためです。


コンテナオブジェクトに対する動作

ネストオブジェクト、MapListなどのコンテナ(値を保持するための器)オブジェクトを扱う際には、以下の場合にエラーになります。


  • コンテナオブジェクトがnullの場合

  • Mapの場合はキーに対応するネストオブジェクトがnullの場合(キーが存在しない場合も同様)

  • Listの場合はindex位置に存在するネストオブジェクトがnullの場合(指定したindex位置に要素が存在しない場合も同様)

この動作を回避するには・・・コンテナオブジェクトには空のオブジェクトを設定しておく必要があります。加えて、MapListにネストオブジェクトを格納する場合は遅延初期化の仕組みを実装しておく必要もあります。なお、本エントリーでは、遅延初期化の仕組みをサポートするために、commons-collectionsのLazyMapLazyListを使いました。


pom.xml

<dependency>

<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.3</version>
</dependency>

public class DemoProperties {

// ...
private final Connection defaultConnection =
new Connection(); // 空のオブジェクトを生成しておく
private final Map<String, Connection> connections =
LazyMap.lazyMap(new HashMap<>(), Connection::new); // 遅延初期化実装のMapを生成しておく
private final List<Template> templates =
LazyList.lazyList(new ArrayList<>(), Template::new); // 遅延初期化実装のListを生成しておく
// ...


付録


同一ファイル内のプロパティをプレースホルダとして指定する方法

PropertyOverrideConfigurerに指定したプロパティファイルにあるプロパティをプレースホルダに指定したい場合は、@PropertySourceまたはPropertySourcesPlaceholderConfigurerを利用して該当ファイルをSpringのプロパティ管理機能の管理下にすることで解決することができます。


@PropertySourceの使用例

@SpringBootApplication

@PropertySource("classpath:demo-properties.properties")
public class DemoApplication {
// ...
}


PropertySourcesPlaceholderConfigurerの使用例

@Bean

static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
@Value("classpath*:*-properties.properties") Resource... resources) {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setLocations(resources);
return configurer;
}

@PropertySourceはワイルドカード指定ができないため一つのファイルしか指定できないのに対して、PropertySourcesPlaceholderConfigurerは複数のファイルを指定することができる仕組みになっています。


コンテナオブジェクトに対するSpringのデフォルト動作を変更する方法

SpringのDIコンテナのデフォルト動作では、コンテナオブジェクトがnullだったり、MapキーやList位置に対応するネストオブジェクトがnullの場合(対応する要素が存在しない場合)にエラーになってしまいます。本編では、空のオブジェクトを設定したり、遅延初期化実装のコレクションを使う方法を紹介しましたが、SpringのDIコンテナの動作を変更することで対応することもできます。ただし・・・ここで紹介する方法は、DIコンテナの全体の動作に影響を与える可能性があるという点は意識しておいたほうがよいでしょう。

Spring Bootアプリケーションであれば、以下のようなApplicationContextの拡張クラスを作成し、Spring Bootが使うコンテキストクラスに指定するだけです。


ApplicationContextの拡張方法

public class MyAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {

public MyAnnotationConfigApplicationContext() {
super(new DefaultListableBeanFactory(){
@Override
protected void initBeanWrapper(BeanWrapper bw) {
super.initBeanWrapper(bw);
bw.setAutoGrowNestedPaths(true); //  ネストオブジェクトが存在しない場合にオブジェクトを生成するように設定する
}
});
}
}


拡張したApplicationContextクラスの適用方法

@SpringBootApplication

public class DemoApplication {

public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(DemoApplication.class)
.contextClass(MyAnnotationConfigApplicationContext.class) // 拡張したApplicationContextクラスを指定
.run(args);
}
}


本エントリーでは非Web環境で使われるApplicationContextクラスを拡張する例になっていますが、実際には利用する環境に合わせた実装クラスを拡張する必要があります。

環境
Spring Bootが適用する実装クラス

Servlet Web
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext

Reactive Web
org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext

スタンドアロン(非Web)
org.springframework.context.annotation.AnnotationConfigApplicationContext

Spring Boot以外での変更方法は割愛しますが・・・・利用するApplicationContextの実装クラスが利用するDefaultListableBeanFactoryinitBeanWrapperメソッドをオーバライドし、BeanWrapperautoGrowNestedPathstrueに設定するようにすれば、同じ動作になります。


まとめ

今お仕事で関わっているアプリ(残念ながら・・・非Spring Bootアプリ)開発で、「Type-safe Configuration Properties」チックなこと(プロパティファイルに指定した値などをJavaオブジェクトへバインド)がしたいな〜と思い、PropertyOverrideConfigurerが使えるんじゃないか!?と思って調べた結果をメモってみました。

最後に・・・Springでアプリケーションを構築する上で、全てのBeanをマニュアルでBean定義することはなく、フレームワークから提供されているBean定義を補助するアノテーションやXMLネームスペースを利用して必要なBeanを定義していきます。その際、Bean自体には動作をカスタマイズするためのプロパティが用意されているけど、アノテーションおよびXMLの属性によってプロパティの値を変えることができない・・・ということが(たまに)あります。

そのようなBeanのプロパティをカスタマイズしたい場合は、PropertyOverrideConfigurerを利用すると簡単にカスタマイズすることができると思います。