Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

Spring Bootの外部設定値(application.yml)のプロファイルの優先順について

スポンサードリンク

f:id:fa11enprince:20140905165611j:plain 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" のように指定すればイケるか?と思ったら、 この指定は全く優先順位に関係がない。

調べると、やっぱりダメみたい。

https://stackoverflow.com/questions/23617831/what-is-the-order-of-precedence-when-there-are-multiple-springs-environment-pro

ベストアンサーになっていた人の主張するベストプラクティスはこんな感じ

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