CLOVER🍀

That was when it all began.

自身のグローバルIPアドレスをコマンドラインで確認する

コマンドラインで、自身のグローバルIPアドレスを調べる方法をメモしておきたいな、と。

こちらで。

$ curl https://ifconfig.me
aaa.bbb.ccc.ddd

余談ですが、HTTPでも見れます。

$ curl ifconfig.me

ブラウザでアクセスすると、より詳細な情報が見れたり。

What Is My IP Address? - ifconfig.me

姉妹サイト。さらに詳しくなります。

Comprehensive IP address data, IP geolocation API and database - IPinfo.io

こちらのサイトでも同じことができます。

$ curl https://ifconfig.co
## IPv6


$ curl -4 https://ifconfig.co
xxx.xxx.xxx.xxx
## IPv4

やっぱり、HTTPでもアクセスできます。

$ curl -4 ifconfig.co

IPv6も見れますが、レートリミットがあり1分間に1リクエストが許可されます。

Please limit automated requests to 1 request per minute. No guarantee is made for requests that exceed this limit. They may be rate-limited, with a 429 status code, or dropped entirely.

What is my IP address? — ifconfig.co

さらに。

$ curl https://inet-ip.info
xxx.xxx.xxx.xxx

$ curl inet-ip.info


$ curl https://httpbin.org/ip
{
  "origin": "xxx.xxx.xxx.xxx"
}


$ curl httpbin.org/ip

レスポンスがJSONで戻ってくるものも。

覚えておくとよいかな、と。

Spring Boot/Spring Frameworkで、プロパティファイルを読み込む(JavaConfig)

これは、なにをしたくて書いたもの?

Spring Frameworkで、プロパティファイルをJavaConfigにマッピングする場合、あとProfileも含めて…という時は
どうするんだっけ?というのをよく忘れるので。

メモしておこうかなと。

Spring Frameworkで、プロパティファイルを扱う

Spring Bootでこう言うと、まず真っ先に挙がるのはapplication.propertiesだと思います。

Externalized Configuration

自分で用意するプロパティファイルを扱う場合は、どうするのでしょう?

あと、Profileについても。

環境に応じて設定値を変更する場合は環境変数で、という話にしたいところかなとは思いますが、これはこれで押さえて
おきたい内容ではあるので今回扱おうと思います。

@PropertySource

自分で用意したプロパティファイルを読む場合は、@PropertySourceアノテーションを使うのかな、と。

Using @PropertySource

PropertySource (Spring Framework 5.3.6 API)

Javadocに書かれているように、@PropertySourceアノテーション+@Configurationアノテーションを使うことで
プロパティファイルの内容をSpringに組み込むことができます。
※@Configurationアノテーションではなく、@Componentアノテーションでもいいのですが

 @Configuration
 @PropertySource("classpath:/com/myco/app.properties")
 public class AppConfig {

また、プロパティファイル内にマルチバイト文字を含む場合は、encodingを指定して読み込むようにすればOKです。

 @Configuration
 @PropertySource(value = "classpath:/com/myco/app.properties", encoding = "UTF-8")
 public class AppConfig {

SpELも使えるので、Profiileごとにプロパティファイルを用意したい場合は、以下のようにすればよいでしょう。

 @Configuration
 @PropertySource(value = "classpath:/com/myco/app-${spring.profiles.active}.properties", encoding = "UTF-8")
 public class AppConfig {
    ....
}

また、複数のプロパティファイルを読み込みたい場合は、@PropertySourcesアノテーションを使うことで
@PropertySourceアノテーションを複数指定することができます。

PropertySources (Spring Framework 5.3.6 API)

こんな感じですね。書いた順に読み込まれる(後勝ち)ようです。

 @Configuration
 @PropertySources({
   @PropertySource(value = "classpath:/com/myco/app.properties", encoding = "UTF-8"),
   @PropertySource(value = "classpath:/com/myco/app-${spring.profiles.active}.properties", encoding = "UTF-8")
 })
 public class AppConfig {
    ....
}

ただ、この書き方だと少なくとも2つのプロパティファイルが存在している必要があり、どちらかが存在しない場合は
例外がスローされます。

それで困る場合は、ignoreResourceNotFound属性をtrueにすれば、指定のファイルが存在しない場合に無視することが
できます。

たとえば、Profile指定のプロパティファイルが存在しないケースがある場合は、以下のような指定になります。

 @Configuration
 @PropertySources({
   @PropertySource(value = "classpath:/com/myco/app.properties"),
   @PropertySource(value = "classpath:/com/myco/app-${spring.profiles.active}.properties", encoding = "UTF-8", ignoreResourceNotFound = true)
 })
 public class AppConfig {
    ....
}
@ConfigurationPropertiesか@Valueか

値の取得には、@ConfigurationPropertiesアノテーションまたは@Valueアノテーションを使います。

ConfigurationProperties (Spring Boot 2.4.5 API)

Value (Spring Framework 5.3.6 API)

2つの方法の違いは、こちらに記述があります。

@ConfigurationProperties vs. @Value

@ConfigurationPropertiesアノテーションを使う場合、Relaxed Binding(プロパティ名から@ConfigurationPropertiesが
付与されたBeanのプロパティに一定のルールでマッピングしてくれる機能)が使える、メタデータの生成ができて補完で
便利などのポイントがあります。

一方で、SpELが使えるのは@Valueだけです。

とはいえ、プロパティを型安全に扱えるのは@ConfigurationPropertiesアノテーションなので、基本的にはこちらを使うのが
よさそうです。

Type-safe Configuration Properties

Third-party Configuration

Relaxed Bindingではprefix以降の部分のプロパティ名は、Kebab case、Camel case、アンダースコア(大文字、小文字)での
変換をサポートしています。

ドキュメントの例でいくと、acme.my-project.person.[プロパティ名]というプロパティキーで@ConfigurationProperties
アノテーションでacme.my-project.personをprefixとした場合、BeanのfirstNameというプロパティには以下の4パターンの
プロパティ名がバインドできます。

  • acme.my-project.person.first-name(推奨)
  • acme.myProject.person.firstName
  • acme.my_project.person.first_name
  • ACME_MYPROJECT_PERSON_FIRSTNAME(環境変数で指定する場合の推奨)

プロパティファイル内で記述する分には、Kebab caseで定義するのが良さそうです。

ちなみに、@ConfigurationPropertiesアノテーションはBeanへ値をバインドする仕組みであるため、値のバインドには
getter/setter、またはコンストラクタでのインジェクションが必要になります。

JavaBean properties binding

Constructor binding

今回はgetter/setterを使おうと思います。

こんな感じの例になりますね。

@ConfigurationProperties(prefix="acme.my-project.person")
public class OwnerProperties {
    // 以下の4つのパターンをバインド可能
    //     acme.my-project.person.first-name
    //     acme.myProject.person.firstName
    //     acme.my_project.person.first_name
    //     ACME_MYPROJECT_PERSON_FIRSTNAME
    private String firstName;

    // getter/setterが必要
    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}
@ConfigurationPropertiesScan

@ConfigurationPropertiesアノテーションを付与したクラスは、@ConfigurationPropertiesScanアノテーションを合わせて
使うことでBeanとして検出・登録することができます。

Enabling @ConfigurationProperties-annotated types

ConfigurationPropertiesScan (Spring Boot 2.4.5 API)

Spring Boot 2.2から使えるアノテーションだそうです。

@ConfigurationProperties scanning

それ以前は、@EnableConfigurationPropertiesアノテーションで同じことをしていたのだとか。

といっても、@ConfigurationPropertiesScanアノテーションは@EnableConfigurationPropertiesアノテーションが付与された
メタアノテーションなんですけどね。

https://github.com/spring-projects/spring-boot/blob/v2.4.5/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java#L46-L47

こんなイメージで使うことになります。

@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApplication {
    ....
}


@ConfigurationProperties(prefix = "acme.my-project")
public class MyProperties {
    ....
}

これで、この例だとMyPropertiesクラスはBeanとして検出されDIが可能になります。

@PropertySourceと@ConfigurationPropertiesScan

ここまで書くと、自分でプロパティファイルを用意した場合は以下のように書けばいいのでは?と思うのですが、これは
うまくいきません。

@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApplication {
    ....
}


@PropertySource(value = "classpath:/com/myco/app.properties", encoding = "UTF-8")
@ConfigurationProperties(prefix = "acme.my-project")
public class MyProperties {
    ....
}

この例でいくとMyPropertiesはBeanとして登録されるのですが、肝心のプロパティ値がインジェクションされない状態に
なってしまいます。

それを回避したければ@Configurationアノテーションを付与するか

@SpringBootApplication
// @ConfigurationPropertiesScan  // この場合、@ConfigurationPropertiesScanは意味をなさなくなる
public class MyApplication {
    ....
}


@Configuration
@PropertySource(value = "classpath:/com/myco/app.properties", encoding = "UTF-8")
@ConfigurationProperties(prefix = "acme.my-project")
public class MyProperties {
    ....
}

@PropertySourceアノテーションを付与するクラスと、@ConfigurationPropertiesアノテーションを付与するクラスを
別々にするか、という気がします。

@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApplication {
    ....
}


@Configuration
@PropertySource(value = "classpath:/com/myco/app.properties", encoding = "UTF-8")
public class MyProperties {
    ....
}


@ConfigurationProperties(prefix = "acme.my-project")
public class MyConfig {
    ....
}

これは、@ConfigurationPropertiesでプロパティをインジェクションする対象として、@PropertySourceを指定した
自身を使うのはダメってことなんでしょうね。

PropertiesFactoryBean

余談的には、Propertiesのインスタンスとして組み込む場合はPropertiesFactoryBeanも使えます。

PropertiesFactoryBean (Spring Framework 5.3.6 API)

と、アノテーションなどの説明はこれくらいにして、実際に試してみましょう。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-72-generic", arch: "amd64", family: "unix"

準備

Spring Initializrで、プロジェクトの作成を行います。今回は、依存関係は特に追加しませんでした。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.4.5 \
  -d javaVersion=11 \
  -d name=profile-spec-configuration \
  -d groupId=org.littlewings \
  -d artifactId=profile-spec-configuration \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.configuration \
  -d baseDir=profile-spec-configuration | tar zxvf -


$ cd profile-spec-configuration
$ find src -name '*.java' | xargs rm

含まれていたソースコードは、1度削除。

Maven依存関係およびプラグインの設定は、こちら。

 <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

こちらをベースに進めていきましょう。

お題

@PropertySourceアノテーションおよび@ConfigurationPropertiesアノテーションを使い、プロパティファイルを読み込みつつ
Beanのプロパティにマッピングしていきます。

この時に、Profileを指定したバリエーションも試してみましょう。

mainクラス

いきなりですが、mainクラスはこのような形で作成。

src/main/java/org/littlewings/spring/configuration/App.java

package org.littlewings.spring.configuration;

import java.util.List;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.core.env.Environment;

@SpringBootApplication
public class App implements CommandLineRunner {
    Environment environment;

    MyConfiguration myConfiguration;
    ProfileSpecConfiguration profileSpecConfiguration;

    public App(
            Environment environment,
            MyConfiguration myConfiguration,
            ProfileSpecConfiguration profileSpecConfiguration
    ) {
        this.environment = environment;
        this.myConfiguration = myConfiguration;
        this.profileSpecConfiguration = profileSpecConfiguration;
    }


    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println("=================================================");
        System.out.printf("current Profile = %s%n", List.of(environment.getActiveProfiles()));

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print MyConfiguration properties");
        System.out.printf("  message = %s%n", myConfiguration.getMessage());
        System.out.printf("  count = %d%n", myConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print ProfileSpecConfiguration properties");
        System.out.printf("  message1 = %s%n", profileSpecConfiguration.getMessage1());
        System.out.printf("  message2 = %s%n", profileSpecConfiguration.getMessage2());
        System.out.printf("  count = %d%n", profileSpecConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print application properties");
        System.out.printf("  spring.application.name = %s%n", environment.getProperty("spring.application.name"));

        System.out.println();
        System.out.println("=================================================");
    }
}

こちらの2つのクラスは、自分で作成したプロパティファイルの値をマッピングしたクラスになります。

    MyConfiguration myConfiguration;
    ProfileSpecConfiguration profileSpecConfiguration;

Environmentは、現在のProfileの確認やapplication.propertiesの確認に使います。

    Environment environment;

Profileを変えたりしつつ、以下の部分で実際に適用されているプロパティ値を確認してきましょう。

    @Override
    public void run(String... args) throws Exception {
        System.out.println("=================================================");
        System.out.printf("current Profile = %s%n", List.of(environment.getActiveProfiles()));

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print MyConfiguration properties");
        System.out.printf("  message = %s%n", myConfiguration.getMessage());
        System.out.printf("  count = %d%n", myConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print ProfileSpecConfiguration properties");
        System.out.printf("  message1 = %s%n", profileSpecConfiguration.getMessage1());
        System.out.printf("  message2 = %s%n", profileSpecConfiguration.getMessage2());
        System.out.printf("  count = %d%n", profileSpecConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print application properties");
        System.out.printf("  spring.application.name = %s%n", environment.getProperty("spring.application.name"));

        System.out.println();
        System.out.println("=================================================");
    }

実行は、以下のコマンドで行います。

## Profileを指定しない場合
$ mvn spring-boot:run


## Profileを指定する場合
$ mvn spring-boot:run -Dspring-boot.run.profiles=[Profile名]

では、プロパティファイルをマッピングするクラスを書いていきます。

単一のプロパティファイルを扱う

まずは、Profileといったものを意識しない、単一のプロパティファイルを扱うクラスを書いていきます。

こんな感じで作成。

src/main/java/org/littlewings/spring/configuration/MyConfiguration.java

package org.littlewings.spring.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource(value = "classpath:/my-configuration.properties", encoding = "UTF-8")
@ConfigurationProperties(prefix = "my.configuration")
public class MyConfiguration {
    String message;
    int count;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

プロパティファイルは、こんな内容で用意。

src/main/resources/my-configuration.properties

my.configuration.message=こんにちは、世界
my.configuration.count=10

@ConfigurationPropertiesScanアノテーションは使用しません。

@SpringBootApplication
public class App implements CommandLineRunner {

これで、以下の内容を確認します。

        System.out.println("=================================================");
        System.out.printf("current Profile = %s%n", List.of(environment.getActiveProfiles()));

        System.out.println();
        System.out.println("=================================================");

        System.out.println("print MyConfiguration properties");
        System.out.printf("  message = %s%n", myConfiguration.getMessage());
        System.out.printf("  count = %d%n", myConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

先ほどソースコードを載せた時に、この後ろにProfileごとにプロパティファイルを用意したクラスの内容を表示する処理が
ありましたが、今回は無視してください。

実行。

$ mvn spring-boot:run

結果。

=================================================
current Profile = []

=================================================
print MyConfiguration properties
  message = こんにちは、世界
  count = 10

=================================================

プロパティファイルの内容が取得できていることが、確認できました。あと、Profileは指定していないので空ですね。

ここで、@Configurationアノテーションを削除して

@PropertySource(value = "classpath:/my-configuration.properties", encoding = "UTF-8")
@ConfigurationProperties(prefix = "my.configuration")
public class MyConfiguration {

@ConfigurationPropertiesScanアノテーションを付与して実行すると

@SpringBootApplication
@ConfigurationPropertiesScan
public class App implements CommandLineRunner {

MyConfigurationクラスをBeanとして扱えてはいるものの、プロパティ値が取得できなくなります。

=================================================
current Profile = []

=================================================
print MyConfiguration properties
  message = null
  count = 0

=================================================

先述の通り、@ConfigurationPropertiesアノテーションを付与したクラスを@ConfigurationPropertiesScanアノテーションで
検出するようにして、かつ一緒に@PropertySourceアノテーションを付与してもうまくいきません。

というわけで、@ConfigurationPropertiesアノテーションを付与するクラスと@PropertySourceアノテーションを
付与するクラスを分離してみます。

@Configuration
@PropertySource(value = "classpath:/my-configuration.properties", encoding = "UTF-8")
class MyConfigurationProperties {}

@ConfigurationProperties(prefix = "my.configuration")
public class MyConfiguration {

mainクラスは、そのままです。

@SpringBootApplication
@ConfigurationPropertiesScan
public class App implements CommandLineRunner {

今度はうまくいきます。

=================================================
current Profile = []

=================================================
print MyConfiguration properties
  message = こんにちは、世界
  count = 10

=================================================

これでちょっと悩んだので、メモ的に…。

Profileに応じたプロパティファイルを読むようにする

次は、Profileに応じたプロパティファイルを読むようにしてみましょう。

mainクラスからは、@ConfigurationPropertiesScanアノテーションは削除しました。

@SpringBootApplication
public class App implements CommandLineRunner {

プロパティファイルは、3つ用意します。

Profileなし。

src/main/resources/profile-spec-configuration.properties

profile.spec.configuration.message1=default message1
profile.spec.configuration.message2=default message2
profile.spec.configuration.count=1

developというProfile相当。Profileなしのものから、ひとつ項目を減らしています。

src/main/resources/profile-spec-configuration-develop.properties

profile.spec.configuration.message1=develop profile message1
profile.spec.configuration.count=5

productionというProfile相当。

src/main/resources/profile-spec-configuration-production.properties

profile.spec.configuration.message1=production profile message1
profile.spec.configuration.message2=production profile only message2
profile.spec.configuration.count=10

用意したプロパティファイルは、Profileなしのもの、Profile指定ありのものを両方読むようにしてみます。

プロパティファイルの内容をマッピングするクラスは、こちら。@PropertySourceアノテーションについては、あとで
記載します。

src/main/java/org/littlewings/spring/configuration/ProfileSpecConfiguration.java

package org.littlewings.spring.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.PropertySources;

@Configuration
// あとで
@ConfigurationProperties(prefix = "profile.spec.configuration")
public class ProfileSpecConfiguration {
    String message1;
    String message2;
    int count;

    public String getMessage1() {
        return message1;
    }

    public void setMessage1(String message1) {
        this.message1 = message1;
    }

    public String getMessage2() {
        return message2;
    }

    public void setMessage2(String message2) {
        this.message2 = message2;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

これで、mainクラスのこちらの部分の出力を確認していきます。

        System.out.println("=================================================");
        System.out.printf("current Profile = %s%n", List.of(environment.getActiveProfiles()));

        System.out.println();
        System.out.println("=================================================");

        // 省略

        System.out.println("print ProfileSpecConfiguration properties");
        System.out.printf("  message1 = %s%n", profileSpecConfiguration.getMessage1());
        System.out.printf("  message2 = %s%n", profileSpecConfiguration.getMessage2());
        System.out.printf("  count = %d%n", profileSpecConfiguration.getCount());

        System.out.println();
        System.out.println("=================================================");

まずは、こちらの定義で。@PropertySourcesアノテーションを使い、@PropertySourceアノテーションを複数指定
できるようにしています。

PropertySources (Spring Framework 5.3.6 API)

@Configuration
@PropertySources({
        @PropertySource("classpath:/profile-spec-configuration.properties"),
        @PropertySource("classpath:/profile-spec-configuration-${spring.profiles.active}.properties")
})
@ConfigurationProperties(prefix = "profile.spec.configuration")
public class ProfileSpecConfiguration {

確認してみます。

develop Profile。

$ mvn spring-boot:run -Dspring-boot.run.profiles=develop

profile-spec-configuration-develop.propertiesファイルとprofile-spec-configuration.propertiesファイルの両方に
定義されているものはprofile-spec-configuration-develop.propertiesファイルの方が優先され、そうでないものは
profile-spec-configuration.propertiesの方が残っていますね。

=================================================
current Profile = [develop]

=================================================
print ProfileSpecConfiguration properties
  message1 = develop profile message1
  message2 = default message2
  count = 5

=================================================

production Profile指定。

$ mvn spring-boot:run -Dspring-boot.run.profiles=production

先ほどの結果から予想できますが、こちらはすべての内容がprofile-spec-configuration-production.propertiesファイルの内容で
上書きされるようです。

=================================================
current Profile = [production]

=================================================
print ProfileSpecConfiguration properties
  message1 = production profile message1
  message2 = production profile only message2
  count = 10

=================================================

では、@PropertySourceアノテーションの順番を入れ替えてみましょう。

@Configuration
@PropertySources({
        @PropertySource("classpath:/profile-spec-configuration-${spring.profiles.active}.properties"),
        @PropertySource("classpath:/profile-spec-configuration.properties")
})
@ConfigurationProperties(prefix = "profile.spec.configuration")
public class ProfileSpecConfiguration {

develop Profile。

$ mvn spring-boot:run -Dspring-boot.run.profiles=develop

すると、内容がすべてprofile-spec-configuration.propertiesファイルのものになります。

=================================================
current Profile = [develop]

=================================================
print ProfileSpecConfiguration properties
  message1 = default message1
  message2 = default message2
  count = 1

=================================================

production Profile指定でも同じです。

$ mvn spring-boot:run -Dspring-boot.run.profiles=production

結果。

=================================================
current Profile = [production]

=================================================
print ProfileSpecConfiguration properties
  message1 = default message1
  message2 = default message2
  count = 1

=================================================

つまり、@PropertySourcesアノテーション内に指定した@PropertySourceアノテーションに定義したプロパティファイル内に
キーが重複したものがあれば、後勝ちになるということですね。

まあ、重複させない方がいいのかな、とは思います…。

ところで、ここまでの定義(どちらでも構いません)で、以下のようなプロパティファイルを用意していないProfileで実行すると

$ mvn spring-boot:run -Dspring-boot.run.profiles=staging

対応するプロパティファイルが存在しない、ということで例外がスローされます。

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [org.littlewings.spring.configuration.App]; nested exception is java.io.FileNotFoundException: class path resource [profile-spec-configuration-staging.properties] cannot be opened because it does not exist
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:189) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:331) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:247) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:311) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:112) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:746) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:564) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:782) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:774) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:339) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1340) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1329) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.littlewings.spring.configuration.App.main(App.java:31) ~[classes/:na]
Caused by: java.io.FileNotFoundException: class path resource [profile-spec-configuration-staging.properties] cannot be opened because it does not exist
    at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:187) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.EncodedResource.getInputStream(EncodedResource.java:159) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.PropertiesLoaderUtils.fillProperties(PropertiesLoaderUtils.java:110) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.PropertiesLoaderUtils.fillProperties(PropertiesLoaderUtils.java:81) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.PropertiesLoaderUtils.loadProperties(PropertiesLoaderUtils.java:67) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.ResourcePropertySource.<init>(ResourcePropertySource.java:67) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.core.io.support.DefaultPropertySourceFactory.createPropertySource(DefaultPropertySourceFactory.java:37) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.processPropertySource(ConfigurationClassParser.java:463) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:280) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:250) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:199) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:304) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:250) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:207) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:175) ~[spring-context-5.3.6.jar:5.3.6]
    ... 13 common frames omitted

つまり、@PropertySourceアノテーションで指定したファイルは、今の定義では必須だということです。

これが嫌な場合、存在しないことがありうるケースでは、@PropertySourceアノテーションのignoreResourceNotFound属性を
trueにするとよいでしょう。

@Configuration
@PropertySources({
        @PropertySource("classpath:/profile-spec-configuration.properties"),
        @PropertySource(value = "classpath:/profile-spec-configuration-${spring.profiles.active}.properties", ignoreResourceNotFound = true)
})
@ConfigurationProperties(prefix = "profile.spec.configuration")
public class ProfileSpecConfiguration {

この場合は、profile-spec-configuration.propertiesファイルだけが必須になっています。

もう1度実行。

$ mvn spring-boot:run -Dspring-boot.run.profiles=staging

今度は実行に成功し、profile-spec-configuration.propertiesファイルの内容が表示されます。
profile-spec-configuration-staging.propertiesファイルという存在しないものは、無視されました、と。

=================================================
current Profile = [staging]

=================================================
print ProfileSpecConfiguration properties
  message1 = default message1
  message2 = default message2
  count = 1

=================================================

application.propertiesはどうやっているのか?

ところでapplication.propertiesについては、Profileに対応していないプロパティファイルが存在していなくてもエラーに
なりません。

というか、けっこう融通効きますよね。どうなっているんでしょう。

こちらを見返してみます。

Externalized Configuration

こんなことが書かれています。

Config data files are considered in the following order: 1. Application properties packaged inside your jar (application.properties and YAML variants). 2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants). 3. Application properties outside of your packaged jar (application.properties and YAML variants). 4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

確認してみましょう。spring.application.nameを扱ってみます。

Profile指定なし。

src/main/resources/application.properties

spring.application.name=Default Application Name

develop Profile(中身なし)。

src/main/resources/application-develop.properties




production Profile。

src/main/resources/application-production.properties

spring.application.name=Production Application Name

mainクラスの、こちらの部分で確認してみましょう。

        System.out.println("=================================================");
        System.out.printf("current Profile = %s%n", List.of(environment.getActiveProfiles()));

        System.out.println();
        System.out.println("=================================================");

        // 省略

        System.out.println("print application properties");
        System.out.printf("  spring.application.name = %s%n", environment.getProperty("spring.application.name"));

        System.out.println();
        System.out.println("=================================================");

Profile指定なし。

$ mvn spring-boot:run

結果。

=================================================
current Profile = []

=================================================
print application properties
  spring.application.name = Default Application Name

=================================================

develop Profile。

$ mvn spring-boot:run -Dspring-boot.run.profiles=develop

結果。

=================================================
current Profile = [develop]

=================================================
print application properties
  spring.application.name = Default Application Name

=================================================

production Profile。

$ mvn spring-boot:run -Dspring-boot.run.profiles=production

結果。

=================================================
current Profile = [production]

=================================================
print application properties
  spring.application.name = Production Application Name

=================================================

つまり、後勝ちですね。

存在しないProfile。

$ mvn spring-boot:run -Dspring-boot.run.profiles=staging

こちらは、問題なく動きます。

=================================================
current Profile = [staging]

=================================================
print application properties
  spring.application.name = Default Application Name

=================================================

ソースコードはどうなっているか?というと、専用の処理が用意されているみたいですね。

https://github.com/spring-projects/spring-boot/blob/v2.4.5/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java

まとめ

Spring Boot/Spring Frameworkで、プロパティファイルをJavaConfigにマッピングする方法、あとProfileの扱い方は?
というのを見返してみました。

非常によく忘れるので…ここまでまとめておけば、メモとしては使えるかな、と。

あと、ちゃんと挙動の確認ができたので、やっておいて良かったかなと思います。