Spring Bootのapplication.ymlの仕組み素敵ですよね。
かなり柔軟性がある。
あるときにアプリケーションをMariaDBとMySQLでどちらも対応できるように外部設定値(Externalized Configuration)
であるapplication.ymlにて制御しようとしたときにハマったので記録に残します。
※この環境はSpring Boot 1のころのHikariCPでない設定の場合ですので注意
背景
もともとMariaDBを使用していたが、宗教的な理由からMySQLを使う必要が出てきた。 MariaDBをデフォルトとし、MySQLは適宜切り替えるようにしたいと思った。
基本的に優先順位はここに書いてある。
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config
環境変数かシステムプロパティでactiveなprofileを切り替えてやればうまくいくはず!
ということで設定ファイルを新たに作った。
最初に思いついた方法
application.ymlがもともとあり、 この中にDB設定も入っているとする(実際にはもっと複雑で分割されていたが…)
application.yml
app: myProperty: ipAddress: 127.0.0.1 port: 8080 spring: jpa: properties: hibernate: show_sql: false use_sql_comments: false format_sql: false datasource: url: jdbc:mariadb://localhost:3306/mysql username: root password: root driverClassName: org.mariadb.jdbc.Driver tomcat: max-active: 15 max-age: 60000 max-idle: 2 max-wait: 10000 min-idle: 2 initial-size: 2 test-on-borrow: true test-on-return: false test-while-idle: true validation-query: "SELECT 1" validation-query-timeout: 1000
こんなのがあったとして、 環境変数が空の場合はMariaDBのドライバを使って接続
上書きしたい文だけMySQLの差分を用意してmysqlを作ればいいんだ! そんなふうに思っていました。
※これは古いバージョンのコネクションプールの周りの設定なので、
Spring Boot2以降でHikariCPの場合はこちらを参照のこと。
GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.
application-mysql.yml
spring: datasource: url: jdbc:mysql://localhost:3307/mysql username: root password: root driverClassName: com.mysql.jdbc.Driver tomcat: max-active: 15 max-age: 60000 max-idle: 2 max-wait: 10000 min-idle: 2 initial-size: 2 test-on-borrow: true test-on-return: false test-while-idle: true validation-query: "SELECT 1" validation-query-timeout: 1000
これでガサっとMySQLのとき(環境変数SPRING_PROFILES_ACTIVEがmysqlのものがあるとき)は MySQLに書き換えられる! ヤッター!!SUCCESS!!だと思っていたのですが… これ、気まぐれな挙動を見せます。
どういうことだ…
デフォルトが勝つ場合とmysqlが勝つ場合がある… じゃあ環境変数やめてシステムプロパティから -Dspring.profiles.active="default,dev" のように指定すればイケるか?と思ったら、 この指定は全く優先順位に関係がない。
調べると、やっぱりダメみたい。
ベストアンサーになっていた人の主張するベストプラクティスはこんな感じ
1. プロファイルに固有ではない、「デフォルト」のBean定義する 2. 環境固有のプロファイルでのBean定義のオーバーライド 3. テスト固有のプロファイル内のBean定義をオーバーライドする
つまり、固有のものは必ず分けろってことです。
検証コードを書きました
以下抜粋です application.yml
app: myProperty: ipAddress: 127.0.0.1 port: 8080 spring: jpa: properties: hibernate: show_sql: false use_sql_comments: false format_sql: false
application-mysql.yml
spring: datasource: url: jdbc:mysql://localhost:3307/mysql username: root password: root driverClassName: com.mysql.jdbc.Driver tomcat: max-active: 15 max-age: 60000 max-idle: 2 max-wait: 10000 min-idle: 2 initial-size: 2 test-on-borrow: true test-on-return: false test-while-idle: true validation-query: "SELECT 1" validation-query-timeout: 1000
aplication-mariadb.yml
spring: datasource: url: jdbc:mariadb://localhost:3306/mysql username: root password: root driverClassName: org.mariadb.jdbc.Driver tomcat: max-active: 15 max-age: 60000 max-idle: 2 max-wait: 10000 min-idle: 2 initial-size: 2 test-on-borrow: true test-on-return: false test-while-idle: true validation-query: "SELECT 1" validation-query-timeout: 1000
※本当はinclude等を使えばtomcatの部分はすっきりすると思います。
エントリポイントのコード
package com.example.externalconfig; import java.util.Arrays; import java.util.stream.Collectors; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.Environment; import com.example.externalconfig.config.AppConfig; @SpringBootApplication public class ExternalConfigApplication { @Autowired Environment env; @Autowired AppConfig appConfig; public static void main(String[] args) { try (ConfigurableApplicationContext ctx = SpringApplication.run(ExternalConfigApplication.class, args)) { ExternalConfigApplication app = ctx.getBean(ExternalConfigApplication.class); app.printProperties(); } catch (Exception e) { e.printStackTrace(); } } private void printProperties() { System.out.println("-------------------------------------------"); // 今回の場合は環境変数で指定したActiveなProfileを取得 String[] profiles = env.getActiveProfiles(); // 以下、Environment経由でPropertyを取得 if (!ArrayUtils.isEmpty(profiles)) { System.out.println("active profiles : " + Arrays.stream(profiles).collect(Collectors.joining(","))); System.out.println("spring.datasource.url : " + env.getProperty("spring.datasource.url")); System.out.println("spring.datasource.driverClassName : " + env.getProperty("spring.datasource.driverClassName")); System.out.println("spring.datasource.tomcat.max-age : " + env.getProperty("spring.datasource.tomcat.max-age")); } System.out.println("spring.jpa.properties.hibernate.show_sql : " + env.getProperty("spring.jpa.properties.hibernate.show_sql")); System.out.println("-------------------------------------------"); // @ConfigurationProperties経由で読込み System.out.println("app.myProperty.ipAddress : " + appConfig.getMyProperty().getIpAddress()); System.out.println("app.myProperty.port : " + appConfig.getMyProperty().getPort()); System.out.println("-------------------------------------------"); } }
結果
SPRING_PROFILES_ACTIVEがmariadbのとき
------------------------------------------- active profiles : mariadb spring.datasource.url : jdbc:mariadb://localhost:3306/mysql spring.datasource.driverClassName : org.mariadb.jdbc.Driver spring.datasource.tomcat.max-age : 60000 spring.jpa.properties.hibernate.show_sql : false ------------------------------------------- app.myProperty.ipAddress : 127.0.0.1 app.myProperty.port : 8080 -------------------------------------------
SPRING_PROFILES_ACTIVEがmysqlのとき
------------------------------------------- active profiles : mysql spring.datasource.url : jdbc:mysql://localhost:3307/mysql spring.datasource.driverClassName : com.mysql.jdbc.Driver spring.datasource.tomcat.max-age : 60000 spring.jpa.properties.hibernate.show_sql : false ------------------------------------------- app.myProperty.ipAddress : 127.0.0.1 app.myProperty.port : 8080 -------------------------------------------
その他参考リンク
[Qiita]Spring-Bootの設定プロパティと環境変数 https://qiita.com/NewGyu/items/d51f527c7199b746c6b6
[Qiita]Spring Boot の application.properties (yml) でプロパティが重複したときの挙動 https://qiita.com/yo1000/items/c511e7f9ff59ab8c3ce3 →ただしこれはあくまでincludeした時の挙動
[Qiita]Spring Bootの外部設定値の扱い方を理解する https://qiita.com/kazuki43zoo/items/0ce92fce6d6f3b7bf8eb