Spring boot : build.gradle と application.yml で同じ値を参照したい。

# やりたいこと
build.gradle と application.yml もとい、Javaプロセス内の両方で使いたい変数が色々ありまして ( DB接続先 とか S3接続先 とか ) 、その記述を一箇所にまとめて参照する。

二箇所以上に記述を分散させてしまうと、環境 ( local や production ) によって切り替えたい時の変更が煩雑になったり、単純に修正箇所が増えてヒューマンエラーにつながったりします。
実際に適当に作って面倒になってきたり、複数人で作っていて似たような定義が出来上がってしまって痛い目を見ました。

つまり

* build.gradle、application.yml ( 及びapplication実行時 ) に共通で使える property 定義であること
* ビルド環境によって値を「簡単」切り替えられる
* 一箇所に定義して記述が「簡単」

ということがやりたかったわけです。
ちなみに「簡単」という条件を除けば[以前の投稿](https://qiita.com/Nabesuke/items/8bfde987f76027ab542e)の方法でも条件を満たしていました。

しかし以前の方法だと property が増えると修正しなければならない箇所が増えて面倒でした。今回はその面倒くささを克服したものになります。

# 環境
- Spring boot 2.1.1.RELEASE
- gradle 4.10.3


# 実装
## Propertyファイルを用意する
先ずは property を定義したファイルを用意します。
`Key = value` の形で定義します。

```default.config.properties
applicationName = "hogehoge"
applicationVersion = "1.0.0"

datasourceUrl = "jdbc:mysql://localhost:13306/hogehoge?useSSL=false"
datasourceUser = "root"
datasourcePassword = "root"
datasourceSchemas = "hogehoge"

s3_accessKey = "test"
s3_secretKey = "test"
s3_serviceEndpoint = "./local"
s3_region = "hoge"
```

### 環境ごとの Property を用意する
`[環境名(productionとか)].config.properties` の形でファイル名を定義しておきます。
こちらは default と比べて上書きしたい property 分のみ記述すれば大丈夫です。

```production.config.properties
datasourceUrl = System.getenv('DATA_SOURCE_HOST') ?: ""
datasourceUser = System.getenv('DATA_SOURCE_USER') ?: ""
datasourcePassword = System.getenv('DATA_SOURCE_PASSWORD') ?: ""
datasourceSchemas = System.getenv('DATA_SOURCE_SCHEMAS') ?: ""

s3_accessKey = System.getenv("S3_ACCESS_KEY") ?: ""
secretKey = System.getenv("S3_SECRET_KEY") ?: ""
s3_serviceEndpoint = System.getenv("S3_SERVICE_END_POINT") ?: ""
s3_region = System.getenv("S3_REGION") ?: ""
```

こちらはSystem.getenv() で環境変数から取得するようにしています。

## application.yml
置き換えたい property の値の Key を @ で括ったものを設定します。

```application.yml
spring:
  profiles:
    active: local, development
  # MySQL接続情報
  datasource:
    url: @datasourceUrl@
    username: @datasourceUser@
    password: @datasourcePassword@

app:
  name: @applicationName@
  version: @applicationVersion@

appConfig:
  s3:
    accessKey: @s3_accessKey@
    secretKey: @s3_secretKey@
    serviceEndpoint: @s3_serviceEndpoint@
    region: @s3_region@
```

## build.properties で export する
※関係箇所のみ記述

```build.gradle
// 1. defaultのプロパティを読み込む
def props = new ConfigSlurper().parse(new File("$project.projectDir/default.config.gradle").toURI().toURL())

// 2. 環境を指定してpropetiesを上書きする
if (hasProperty('env')) {
  def envConfig = new ConfigSlurper().parse(new File("$project.projectDir/${env}.config.gradle").toURI().toURL())
  props = props + envConfig
}

// 3. application.ymlのプロパティを上書きする
processResources {
  filesMatching('**/application.yml') {
    filter(
        ReplaceTokens,
        tokens: props
    )
  }
}
```

### ConfigSlurper()
Groovy のメソッドで property を記述した `default.config.gradle` のような感じで定義しておくと読み込んでアクセスできるようにしてくれるやつです。
今回はシンプルな書き方にしてますが、ドット区切りで階層を指定したりと柔軟な書き方が可能です。

### hasProperty('env')
これはビルド引数に env が指定されているかを確認しています。 `./gradlew build -P env=production` のようにして指定します。

### 環境を指定してpropetiesを上書きする ところ
同じ様に `ConfigSlurper()` で該当環境と同じファイル名の property ファイルを読み込んで結合しています。 `+` で結合することによって Key が同じ値が上書きされます。

### application.ymlのプロパティを上書きする ところ
- `filesMatching('**/application.yml')` でapplication.ymlとう名前にマッチするファイルすべてを対象にしています。
- `filter()` で一行ずつ置換します。
第1引数の `ReplaceTokens` は ant  API で、第2引数の tokens で対象文字列を置き換える(KVS → [key: value, key2: value2]の形であればよい)
第2引数の `props`はConfigSlurper()で読み込んだ値が KVS の形で入っている

## application 内で property を使う設定
こちらは application.yml をjava側から読み込む方法を他の方々が書かれているのでそちらをご参照ください。

## これで clean build してあげればお終い

# おわり
最初は延々と誰か他にやっている人がいないか探していたんですが、なかなかピンポイントの記事が見つからず途方に暮れていました。しかし、よくよく考えてみたら Gradle は Groovy で書かれているので、 Groovy で出来ることはなんでもできる事に気づいてこの方法にたどり着きました。
Gradle を 検索ワードにして探していたときは見つからなかったことも Groovy に対して検索するとあっという間に見つかり、またやれることが広がった気がします。

詰まったら視点を変えてみる。精進が足りない。。。

# omake : サブモジュールのビルド時でも同じ値を使いまわす
- rootディレクトリの build.gradle で config を読み込む。
- props(変数名は何でもいい)  を定義して代入しているのでサブモジュール側でアクセスする
- アクセスする方法は サブモジュール側の build.gradle で `rootProject.ext.props` で見れる。
- application.yml への export はよしなに。自分は置換したい値のあるサブモジュール側の build.gradle に `filesMatching()`を書いた。