Spring Bootには「Type-safe Configuration Properties」という仕組みがあり、外部設定のプロパティ値(プロパティファイル、システムプロパティ、環境変数などに設定した値)を「@ConfigurationProperties
を付与したBean」と「@ConfigurationProperties
を付与して生成したBean」のプロパティに設定することができます。
@Component
@ConfigurationProperties("demo")
public class DemoProperties {
private String foo;
// ...
}
@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定義とプロパティファイルを用意すれば設定値を変えることができます。
@Bean
static PropertyOverrideConfigurer propertyOverrideConfigurer(@Value("classpath*:demo-properties.properties") Resource... resources) {
PropertyOverrideConfigurer configurer = new PropertyOverrideConfigurer();
configurer.setLocations(resources);
return configurer;
}
demoProperties.foo=bar
プロパティキーのルール
PropertyOverrideConfigurer
を使用してプロパティ値をBeanへ上書きするためには、指定したプロパティファイルに「{Bean名}.{Beanのプロパティへのパス}
」というキーで値を指定する必要があります。もちろんネストしたオブジェクト、Map
、List
などにも対応しています。
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
demoProperties.connections[github].host=github.com
demoProperties.connections[github].port=443
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を利用しているアプリケーションなら、
demoProperties.defaultConnection.host=${default.host:localhost}
default.host=myhost
とすると、実行時には以下のように解釈されmyhost
が設定されます。
仮にプレースホルダに指定したプロパティキーが存在が存在しない場合でも、デフォルト値を指定しておけばエラーにはならずにlocalhost
が適用されるため、デフォルト値にはローカルの開発環境向けの設定値を指定しておくとよいでしょう。
demoProperties.defaultConnection.host=myhost
ただし、PropertyOverrideConfigurer
に指定したプロパティファイルの中で定義しているプロパティキーは、プロパティプレースホルダに指定することはできません。これは、PropertyOverrideConfigurer
に指定したプロパティファイルが、Spring(Spring Boot)のプロパティ管理機能で管理するプロパティ値として扱われないためです。
コンテナオブジェクトに対する動作
ネストオブジェクト、Map
、List
などのコンテナ(値を保持するための器)オブジェクトを扱う際には、以下の場合にエラーになります。
- コンテナオブジェクトが
null
の場合 - Mapの場合はキーに対応するネストオブジェクトが
null
の場合(キーが存在しない場合も同様) - Listの場合はindex位置に存在するネストオブジェクトが
null
の場合(指定したindex位置に要素が存在しない場合も同様)
この動作を回避するには・・・コンテナオブジェクトには空のオブジェクトを設定しておく必要があります。加えて、Map
とList
にネストオブジェクトを格納する場合は遅延初期化の仕組みを実装しておく必要もあります。なお、本エントリーでは、遅延初期化の仕組みをサポートするために、commons-collectionsのLazyMap
とLazyList
を使いました。
<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のプロパティ管理機能の管理下にすることで解決することができます。
@SpringBootApplication
@PropertySource("classpath:demo-properties.properties")
public class DemoApplication {
// ...
}
@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が使うコンテキストクラスに指定するだけです。
public class MyAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {
public MyAnnotationConfigApplicationContext() {
super(new DefaultListableBeanFactory(){
@Override
protected void initBeanWrapper(BeanWrapper bw) {
super.initBeanWrapper(bw);
bw.setAutoGrowNestedPaths(true); // ネストオブジェクトが存在しない場合にオブジェクトを生成するように設定する
}
});
}
}
@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
の実装クラスが利用するDefaultListableBeanFactory
のinitBeanWrapper
メソッドをオーバライドし、BeanWrapper
のautoGrowNestedPaths
をtrue
に設定するようにすれば、同じ動作になります。
まとめ
今お仕事で関わっているアプリ(残念ながら・・・非Spring Bootアプリ)開発で、「Type-safe Configuration Properties」チックなこと(プロパティファイルに指定した値などをJavaオブジェクトへバインド)がしたいな〜と思い、PropertyOverrideConfigurer
が使えるんじゃないか!?と思って調べた結果をメモってみました。
最後に・・・Springでアプリケーションを構築する上で、全てのBeanをマニュアルでBean定義することはなく、フレームワークから提供されているBean定義を補助するアノテーションやXMLネームスペースを利用して必要なBeanを定義していきます。その際、Bean自体には動作をカスタマイズするためのプロパティが用意されているけど、アノテーションおよびXMLの属性によってプロパティの値を変えることができない・・・ということが(たまに)あります。
そのようなBeanのプロパティをカスタマイズしたい場合は、PropertyOverrideConfigurer
を利用すると簡単にカスタマイズすることができると思います。