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

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

```java:@ConfigurationPropertiesを付与したBean例
@Component
@ConfigurationProperties("demo")
public class DemoProperties {
  private String foo;
  // ...
}
```

```java:@ConfigurationPropertiesを付与して生成したBean例
@ConfigurationProperties("demo")
@Bean
DemoProperties demoProperties() {
  return new DemoProperties();
}
```

```properties:プロパティ値の指定例
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定義ファイル内で設定値をハードコーディングしていたとしても・・・

```java
@Bean
DemoProperties demoProperties() {
  DemoProperties　properties = new DemoProperties();
  properties.setFoo("hoge");
  return properties;
}
```

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

```java:PropertyOverrideConfigurerのBean定義例
@Bean
static PropertyOverrideConfigurer propertyOverrideConfigurer(@Value("classpath*:demo-properties.properties") Resource... resources) {
  PropertyOverrideConfigurer configurer = new PropertyOverrideConfigurer();
  configurer.setLocations(resources);
  return configurer;
}
```

```properties:プロパティ値の指定例(src/main/resources/demo-properties.properties)
demoProperties.foo=bar
```

# プロパティキーのルール

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



```java
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;
    // ...
  }

}
```

```properties:ネストしたオブジェクトに値を設定する際のプロパティ定義例
demoProperties.defaultConnection.host=localhost
demoProperties.defaultConnection.port=9999
```

```properties:Mapに値を設定する際のプロパティ定義例
demoProperties.connections[github].host=github.com
demoProperties.connections[github].port=443
```
```properties: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コンテナに存在しない場合、無効なプロパティキーとして扱われデフォルトの動作ではエラーになります。

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

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

```java
@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定義にすることで変更することができます。

```java
@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)のプロパティ管理機能で管理するプロパティ値として扱われないためです。

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

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

* コンテナオブジェクトが`null`の場合
* Mapの場合はキーに対応するネストオブジェクトが`null`の場合（キーが存在しない場合も同様）
* Listの場合はindex位置に存在するネストオブジェクトが`null`の場合（指定したindex位置に要素が存在しない場合も同様）

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

```xml:pom.xml
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.3</version>
</dependency>
```

```java
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のプロパティ管理機能の管理下にすることで解決することができます。

```java:@PropertySourceの使用例
@SpringBootApplication
@PropertySource("classpath:demo-properties.properties")
public class DemoApplication {
  // ...
}
```

```java: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が使うコンテキストクラスに指定するだけです。

```java:ApplicationContextの拡張方法
public class MyAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {
  public MyAnnotationConfigApplicationContext() {
    super(new DefaultListableBeanFactory(){
      @Override
      protected void initBeanWrapper(BeanWrapper bw) {
        super.initBeanWrapper(bw);
        bw.setAutoGrowNestedPaths(true); // 　ネストオブジェクトが存在しない場合にオブジェクトを生成するように設定する
      }
    });
  }
}
```

```java:拡張した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`の実装クラスが利用する`DefaultListableBeanFactory`の`initBeanWrapper`メソッドをオーバライドし、`BeanWrapper`の`autoGrowNestedPaths`を`true`に設定するようにすれば、同じ動作になります。

# まとめ

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

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