kotlin × Spring Bootでapplication.ymlから設定値を取得する




`application.yml`から値を取得する方法は主に以下の3つのやり方があるようです。

- `@Value`アノテーション
- `Environment`クラス
- `@ConfigurationProperties`アノテーション

それぞれの方法をkotlinで試してみます。

# 環境

- JDK: 1.8
- kotlin: 1.3
- Spring Boot 2.1.4.RELEASE

以下のapplication.ymlから値を取得します。

```yml:application.yml
app-conf:
  numeric-param: 10
  string-param: hogehoge
  structure-param:
    param1: hoge
    param2: fuga
```

# Value

`@Value`アノテーションを使用すると簡単に値を取得できます。

```kotlin:ValueComponent.kt
@Component
class ValueComponent {

    @Value("\${app-conf.numeric-param}")
    val numericParam = 0

    @Value("\${app-conf.string-param}")
    val stringParam = ""

    @Value("\${app-conf.undefined-param:defaultValue}")
    val undefinedParam = ""

    @Value("\${app-conf.structure-param.param1}")
    val structureParam1 = ""

    @Value("\${app-conf.structure-param.param2}")
    val structureParam2 = ""

    fun main() {
        println("numericParam=$numericParam")
        println("stringParam=$stringParam")
        println("undefinedParam=$undefinedParam")
        println("structureParam.param1=$structureParam1")
        println("structureParam.param2=$structureParam2")
    }
}
```

```text:実行結果
numericParam=10
stringParam=hogehoge
undefinedParam=defaultValue
structureParam.param1=hoge
structureParam.param2=fuga
```

`@Value`に渡す文字列は`\${}`のようにエスケープする必要があります。
またvalで定義した変数であっても値をセットしてくれますが、変数定義時にセットしている値は無視されます。
デフォルト値を設定したい場合は`\${変数名:デフォルト値}`のようにします。


# Environment

取得するパラメータを動的に変更したい場合は`Environment.getProperty()`が使えます。

```kotlin:EnvironmentComponent.kt
@Component
class EnvironmentComponent {

    @Autowired
    lateinit var environment: Environment

    fun main() {
        val numericParam = environment.getProperty("app-conf.numeric-param", Int::class.java, 0)
        val stringParam = environment.getProperty("app-conf.string-param", String::class.java, "")
        val undefinedParam = environment.getProperty("app-conf.undefined-param", String::class.java, "defaultValue")
        val structureParam1 = environment.getProperty("app-conf.structure-param.param1", String::class.java, "")
        val structureParam2 = environment.getProperty("app-conf.structure-param.param2", String::class.java, "")

        println("numericParam=$numericParam")
        println("stringParam=$stringParam")
        println("undefinedParam=$undefinedParam")
        println("structureParam.param1=$structureParam1")
        println("structureParam.param2=$structureParam2")
    }
}
```

```text:実行結果
numericParam=10
stringParam=hogehoge
undefinedParam=defaultValue
structureParam.param1=hoge
structureParam.param2=fuga
```

`getProperty()`の第一引数にキー、第二引数に取得する型のClass、第三引数にデフォルト値を渡します。
なお`getProperty()`メソッドはデフォルト値を省略した場合は戻り値がnullableになり、キーが未定義の場合nullが帰ります。
未定義の場合にエラーを発生させたいときは`getProperty()`の代わりに`getRequiredProperty()`を使用します。

```kotlin
// 未定義の場合エラーになる
val undefinedParam = environment.getRequiredProperty("app-conf.undefined-param", String::class.java)
```

# ConfigurationProperties

`@ConfigurationProperties`アノテーションを使用すると複数のパラメータをまとめて取得することができます。

```kotlin:ConfigurationPropertiesComponent.kt
@Component
class ConfigurationPropertiesComponent {

    @Autowired
    lateinit var appConfig: AppConfig

    fun main() {
        println("numericParam=${appConfig.numericParam}")
        println("stringParam=${appConfig.stringParam}")
        println("undefinedParam=${appConfig.undefinedParam}")
        println("structureParam.param1=${appConfig.structureParam.param1}")
        println("structureParam.param2=${appConfig.structureParam.param2}")
    }
}

@Configuration
@ConfigurationProperties("app-conf")
class AppConfig {

    var numericParam = 0

    var stringParam = ""

    var undefinedParam = "defaultValue"

    val structureParam = StructureParam()
}

class StructureParam {
    var param1 = ""
    var param2 = ""
}
```

```text:実行結果
numericParam=10
stringParam=hogehoge
undefinedParam=defaultValue
structureParam.param1=hoge
structureParam.param2=fuga
```

パラメータを受け取るクラスに`@ConfigurationProperties`アノテーションを付与します。
フィールドへはsetter経由で値が設定されるため`val`は使用できません。(階層化されたプロパティを除く)
`appConfig.structureParam.param1`、`appConfig.structureParam.param2`のように階層構造をもったパラメータも取得できます。
ただし、パラメータを受け取るためのインスタンスは自動生成してくれないので、あらかじめインスタンスを生成しておく必要があります。(こちらは`val`で定義できます)
また、パラメータ名とフィールド名は厳密に同じでなくてもある程度までいい感じに認識してくれます。

### 追記
Spring Boot 2.2系から`@ConstructorBinding`が追加されたため、`@ConfigurationProperties`でdata classが利用可能になりました。
参考 : [Spring Boot 2.2系で@ConfigurationPropertiesを使う](https://qiita.com/dais39/items/158e34e86096beae84af)

```kotlin

@SpringBootApplication
// メインクラスに@ConfigurationPropertiesScanをつける必要があります
@ConfigurationPropertiesScan
class DemoApplication

// ...mainメソッド略...

@Component
class ConfigurationPropertiesComponent {

    @Autowired
    lateinit var appConfig: DataClassConfig

    fun main() {
        println(appConfig)
    }
}

// @ConfigurationPropertiesに@ConstructorBindingを追加
@ConstructorBinding
@ConfigurationProperties("app-conf")
data class DataClassConfig(
    val numericParam: Int,
    val stringParam: String,
    val undefinedParam: String = "defaultValue",
    // 階層構造を持つパラメータも事前のインスタンス化は不要です
    val structureParam: DataClassStructureParam
)

data class DataClassStructureParam(
    val param1: String,
    val param2: String
)

```
```text:実行結果
DataClassConfig(numericParam=10, stringParam=hogehoge, undefinedParam=defaultValue, structureParam=DataClassStructureParam(param1=hoge, param2=fuga))
```

# まとめ

application.ymlから値を取得する3つの方法をkotlinで試してみました。
それぞれ一長一短あるので適切に使い分けて利用したいです。

# 参考

[50.5 @ConfigurationProperties](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html#boot-features-kotlin-configuration-properties)
[Spring Bootの外部設定値の扱い方を理解する](https://qiita.com/kazuki43zoo/items/0ce92fce6d6f3b7bf8eb)