このページに書いてあること
Spring MVCで設定ファイルを読み込むときに、条件に応じて使用する設定情報を使い分けたい場合があります。
例えば、社内環境用のアプリと社外環境用のアプリ(以降、内部と外部)が同インスタンスにしており、経路によって使用する設定を切り替えたい場合です。内部と外部で設定ファイル名を分けてapplication_intra.properties/application_internet.propertiesを読み込みたい場合ですね。
そんな時は、PathMatchingResourcePatternResolverを使うとできる可能性があります。
前提
- SpringMVCのバージョンは5.1.19.RELEASE
- application_intra.properties/application_internet.propertiesで個別のクラスを作りたくない(設定ファイルが追加されたら大変)
一般的な設定ファイル読み込み
Spring MVCではいくつかの設定ファイル読み込み方法があるのですが、一般的なものはアノテーションによるものだと思います。
@Configuration
@PropertySource("classpath:application.properties")
@Configurationにより起動時の設定処理であることを通知し、起動時に実行してもらいます。
@PropertySourceにより読み込む設定ファイルを指定しています。("classpath:application.properties")の記述は、クラスパス配下からapplication.propertiesの名前に一致するファイルを読み込む設定です。
で、このときの問題が読み込む設定ファイルをこの書き方は設定ファイル名が固定ということです。
外部向けインスタンスと内部向けインスタンスを作りたい場合、ここの記述を変えないといけないということになります。
こちらの期待としては、ワイルドカードなどを使って全部読み込んでほしいところです。
@PropertySource("classpath:application_*.properties")
旧Spring MVC 5.1.19では、この @PropertySource の書き方でワイルドカードを展開できませんでした。Spring Framework 6.1以降は @PropertySource でもパターン指定を使えます。
複数のファイルを読み込むことも可能です。Springの4以降で使用できる@PropertySourcesを使います。
@PropertySources({
@PropertySource("classpath:application.properties"),
@PropertySource("classpath:another.properties")
})
ただ、見ての通りでこちらは設定ファイル名をベタ書きする必要があるので、今後設定ファイルが増えていくとキツイです。
PathMatchingResourcePatternResolverによる読み込み
実行用サンプル
配布コードは実物のjarをその場で作り、中の設定をストリームで読みます。同じ経路が別jarに重複した場合は起動順に任せず失敗します。 検証コードを見る。 JDK 17とMaven 3.9系で試せます。ソース・テスト・pom.xml・READMEをZIPに収録しています。
社内・社外のファイルに同じ target キーがあっても、経路ごとに別の値として保存します。同名の経路ファイルが2つある場合はエラーにし、どちらを採るかを検出順で決めません。
今回の要件は、社内・社外の設定を読み込み、接続経路ごとに別々の値として取り出すことです。そのため、ファイルを列挙して名前ごとに分けて保持します。
PathMatchingResourcePatternResolver が、指定したパターンに合う Resource を列挙します。
jarに収録された Resource は通常のファイルパスとして開けないことがあるため、Resource#getInputStream() で読み込みます。
ChannelSettingsLoader.java の全文
package info.syachiku.maintenance;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.springframework.core.io.support.ResourcePatternResolver;
class ChannelSettingsLoader {
private final ResourcePatternResolver resolver;
ChannelSettingsLoader(ResourcePatternResolver resolver) {
this.resolver = resolver;
}
Map<String, Properties> load(String pattern) throws IOException {
Map<String, Properties> settings = new LinkedHashMap<>();
for (var resource : resolver.getResources(pattern)) {
String fileName = resource.getFilename();
if (fileName == null || !fileName.matches("application_[a-z]+\\.properties")) {
throw new IllegalArgumentException("Unexpected configuration file: " + fileName);
}
String channel = fileName.substring("application_".length(), fileName.indexOf('.'));
Properties properties = new Properties();
try (var reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
properties.load(reader);
}
if (settings.putIfAbsent(channel, properties) != null) {
throw new IllegalArgumentException("Duplicate channel: " + channel);
}
}
return Map.copyOf(settings);
}
}この例では、
classpath*:channels/application_*.properties
このパターンで channels/application_intra.properties と channels/application_internet.properties を検出します。ファイル名から経路名を取り、別々の Properties に格納します。
探索に使う * はパスのパターンで、正規表現ではありません。ここでは classpath*: を使い、jarに含まれるリソースも対象にします。別の場所を対象にする場合は、配備形態に合う file: 等の指定を選びます。
ファイル名を何らかのルールに基づいて動的に決めたい場合はこのようにするといいかもしれません。
