電通総研 テックブログ

電通総研が運営する技術ブログ

MicroProfile Configで始めるJavaアプリケーションの設定管理

みなさんこんにちは、電通総研コーポレート本部システム推進部の佐藤太一です。

設定管理は開発段階だけでなく、運用フェーズでも重要な課題です。MicroProfile Configを活用すれば、設定値の変更が及ぼす影響を最小限に抑えつつ、柔軟な設定管理が実現できます。

このブログエントリでは、MicroProfile Configの基本概念と使い方を初心者向けに解説します。アプリケーションの設定管理を効率化するための第一歩を踏み出しましょう。

はじめに

まずは、MicroProfile Configの概要と利用目的について説明します。

MicroProfile Configの概要

MicroProfile ConfigはJakarta EEのMicroProfileに含まれているライブラリです。

このライブラリは、アプリケーションの設定値を外部化し、一元管理するための仕組みを提供します。設定値をソースコードから分離すると、アプリケーションの保守性や柔軟性が高まるでしょう。

標準で提供されている設定ソースは、システムプロパティ、プロパティファイル、環境変数です。ただ、設定ソースは柔軟に拡張できるので、データベースから値を取り出したりAWS AppConfigのようなクラウドプロバイダー固有のサービスから値を取りだすような事も簡単にできます。

実は、MicroProfile Configは仕様だけが存在するものです。今回の説明に使うのは smallrye-config というRed Hatが実装しているライブラリです。

MicroProfile Configを使う目的

MicroProfile Configを使う理由は、いくつかあります。

まず、設定値の変更に伴うソースコードの修正を最小限に抑えることで保守性を向上します。さらに、環境毎の設定切り替えが容易になるのでデプロイメントの柔軟性が高まるでしょう。

また、設定された値の型を検証したり、設定値を文字列からアプリケーションで利用する型に自動変換できるので設定ミスによる動作不良を最小限に抑えられます。

加えて、動的な設定変更が必要になるような高い可用性を要求するアプリケーションにも対応できます。

アプリケーション開発環境

具体的なソースコードを交えた説明をするにあたって、アプリケーションのビルド環境を作りましょう。

エディタはどんなものでも構いませんが、Java21を使ってMavenでプロジェクトを作成します。

プロジェクトのルートディレクトリに配置する pom.xml は以下のような内容です。

<project
  xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
>
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>mp-config-example</artifactId>
  <version>0.1.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>io.smallrye.config</groupId>
      <artifactId>smallrye-config</artifactId>
      <version>3.8.2</version>
    </dependency>
  </dependencies>
</project>

プロジェクトのルートディレクトリから src/main/javaディレクトリを作成してソースコードの格納ディレクトリとします。

そのまま com/example/config とさらにディレクトリを掘って、その中に Main.javaというファイルで以下の内容を保存します。

package com.example.config;

import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var name = config.getValue("com.example.name", String.class);
        System.out.printf("Hello, %s%n", name);
    }
}

これが MicroProfile Config を利用する最小限のコードです。簡単ですね。

このまま動かすと com.example.name という設定値が必須項目となりますので、以下のようなエラーになります。

Exception in thread "main" java.util.NoSuchElementException: SRCFG00014: The config property com.example.name is required but it could not be found in any config source
    at io.smallrye.config.SmallRyeConfig.convertValue(SmallRyeConfig.java:435)
    at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:380)
    at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:359)
    at com.example.config.Main.main(Main.java:9)

このエラーに対処するためシステムプロパティに -Dcom.example.name=John を設定してアプリケーションを起動してみましょう。以下のように出力されれば、正しく動作しています。

Hello, John

システムプロパティを起動時に設定するのはやめて、環境変数COM_EXAMPLE_NAME=Alice を設定してからアプリケーションを起動してみましょう。以下のように出力されるはずです。

Hello, Alice

これで、MicroProfile Config を使ったアプリケーションの基本的な動作確認は終わりです。非常に簡単ですね。

CDIを使った設定値の取得方法もあるのですが、今回は説明しません。気になる方は仕様を見てください。 参照先:Aggregate related properties into a CDI bean

設定ソース

開発環境をセットアップして設定値を取りだす方法が分かった所で、設定値を保持する仕組みについて説明します。

標準サポートの設定ソース

MicroProfile Configが標準で提供している設定ソースを優先度順に並べると以下の3つです。

優先順位がどのように作用するか、具体的に考えてみましょう。

MicroProfile Configがデフォルトで読むプロパティファイルは META-INF/microprofile-config.properties です。環境変数システムプロパティに設定がなければ、このファイルに記述された内容が使われますので、ローカルマシンを使った開発ではプロパティファイルを使うのが便利でしょう。IDEからmainメソッドのあるクラスを実行する際にパラメータを渡すのは、少しだけ面倒ですよね。

アプリケーションをデプロイするにあたってDockerコンテナにパッケージングするとしましょう。
コンテナオーケストレーションツールを使うとコンテナをデプロイする時に環境変数を追加したり、環境毎に値を切り替えたりするのは非常に簡単です。プロパティファイルに記載があるものをコンテナ起動時に置き換えられます。少し奇妙ですが、MicroProfile Configでは環境変数から値を読む際にはキー名の中に含まれる . (ドット)を _ (アンダースコア)に置き換え小文字は大文字に置き換えて処理をします。
例えば、 com.example.name を処理する際には、環境変数COM_EXAMPLE_NAME が定義されている事を期待して処理を行います。

コンテナオーケストレーションツールを使っているなら、環境変数の置き換えで基本的には対応できるはずです。しかし、緊急避難的に特定のコンテナだけ少し違ったパラメータで動作させたいことは、障害時にはよくあります(障害自体は発生しないで欲しいものですが…)。こういう時にはコンテナの起動パラメータにシステムプロパティを設定することで、既存の設定を上書きできるのです。

なお、システムプロパティの値を使う際には、環境変数のようなドットからアンダースコアへの置き換えはありません。

カスタム設定ソースの作成

MicroProfile Configが標準で提供している設定ソースは全て静的なものです。つまり、設定値を変更した際にJVMやコンテナを再起動する必要があります。

コンテナを使ってデプロイしているアプリケーションがクラスタ化されているなら、再起動することはそれほど難しくないでしょうが、様々な事情によって再起動できないアプリケーションは存在しますよね。

カスタム設定ソースを実装すると、こういう要件に対応できます。

この記事では、長くなり過ぎるのでAWSのAppConfigをMicroProfile Configから利用するためのカスタムソースを実装する記事をご紹介します。

型変換

設定ソースから取りだす際に、文字列としてだけ値を取りだすとアプリケーション内では、そのまま使えない事が多いでしょう。

ここでは、数値や日付として設定値を取りだす方法について説明します。

標準サポートの型変換

まずは、MicroProfile Configが標準でサポートしている型変換を確認してみましょう。

標準で全てのプリミティブ型がサポートされています。つまり、boolean、byte、short、int、long、float、double、charに設定値を自動変換できるのです。同じようにラッパー型であるBoolean、Byte、Short、Integer、Longなども使えます。

特に難しいことはありません、値を取りだす際の第二引数にクラス参照を渡すだけです。例えば以下のようなコードになりますね。

package com.example.config;

import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var value = config.getValue("com.example.intvalue", int.class);
        System.out.printf("Value is %s%n", value);
    }
}

設定ソースに一切の値が設定されていない場合のデフォルト値を用意したいことがあります。

通常はOptional型でラップされたものを取りだすための getOptionalValue メソッドを使えばいいのですが、プリミティブ型の時だけは専用のOptional型を使うので少しトリッキーです。以下のようなコードになります。

package com.example.config;

import java.util.OptionalInt;
import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var value = config.getValue("com.example.undefvalue", OptionalInt.class).orElse(30);
        System.out.printf("Value is %s%n", value);
    }
}

標準でサポートされている型変換としてはClassもあります。Classを指定して値を取りだす際に使われるクラスローダーはThreadから取得できるContextClassLoaderです。

カンマ区切りされた設定値をListや配列として取りだすこともできます。

以下のコードをシステムプロパティに -Dcom.example.listValue=3.1,5.2,4.4,6.8 と設定して実行してみましょう。設定値をカンマ区切りする際に余分なスペースを入れると期待通りに動作しませんので注意してください。

package com.example.config;

import java.util.List;
import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        List<Float> value = config.getValues("com.example.listValue", Float.class);
        System.out.println("values are " + value);
    }
}

このコードで使われているのは、getValuessメソッドです。複数形のsが付いていますね。見逃し易いので注意してください。これを実行すると以下のように標準出力されるはずです。

values are [3.1, 5.2, 4.4, 6.8]

smallrye-config に実装済の型変換

数値型とClass型だけでは、少々不便です。MicroProfile Configの実装であるsmallrye-configには、よく使われる型の変換がいくつか実装されています。

追加でサポートされているクラスは以下のとおりです。

  • java.net.InetAddress
    • IPv4アドレスとIPv6アドレスの両方を扱えます。
  • java.util.UUID
  • java.util.Currency
  • java.util.BitSet
  • java.util.regex.Pattern
  • java.nio.file.Path
    • ローカルのファイルを扱いたいときに使います。

どれも非常に良く使うものばかりですよね。

自動的に行われる型変換

MicroProfile Configには規約に基づく自動的な型変換機能があります。自分達で定義した型を使いたいときには、この規約をうまく利用しましょう。

より高度なカスタムの型変換機能もありますが、あまり使わないので今回は紹介しません。

Enumを使う

まずは、Enumを使うケースを考えてみましょう。以下のようなEnumを定義します。

package com.example.config;

public enum Cats {
    Abyssinian, American_Shorthair, Bengal_Cat, Himalayan;
}

普段使っているものと大きな差分はないでしょう。これを使うコードを見てみましょう。

package com.example.config;

import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var value = config.getValue("com.example.cat", Cats.class);
        System.out.println(value);
    }
}

このコードを動かす際には、システムプロパティとして -Dcom.example.cat=American-Shorthair を設定してください。実行結果として以下のように標準出力されるはずです。

American_Shorthair

システムプロパティとしては、ハイフンを使っていましたが、アンダースコアに読み替えた上でEnumのメンバが選択されていますね。

コンストラクタを使う

独自の型に型変換を行うなら引数の型として String もしくは CharSequence を受取るコンストラクタを定義するのが簡単です。

例えば、標準でサポートされている型の中には java.math.BigDecimal は含まれていませんが、Stringを引数に受け取るコンストラクタが定義されているので型の自動変換ができます。

ちょっと使ってみましょう。

package com.example.config;

import java.math.BigDecimal;
import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var value = config.getValue("com.example.number", BigDecimal.class);
        System.out.println(value);
    }
}

このコードを動かす際には、システムプロパティとして -Dcom.example.number=3344.222 を設定してください。そうすると以下のように標準出力されるでしょう。

3344.222

興味深い振る舞いですよね。

ファクトリメソッドを使う

次はファクトリメソッドが自動的に使われるケースを見てみましょう。

MicroProfileでは public static なメソッドが定義されているとファクトリメソッドとして利用します。メソッド名として有効なのは

  • of
  • valueOf
  • parse

です。引数は一つであることを仮定しています。そして、引数の型として有効なのは String もしくは CharSequence です。

例えば、こういうコードになります。

package com.example.config;

public class Dogs {
    String group;
    
    public static Dogs of(String value) {
        var d = new Dogs();
        // 少し奇妙なコードだが確実にファクトリメソッドが動いていることが分かるようにコンストラクタを使わない
        d.group = value; 
        return d;
    }
    
    @Override
    public String toString() {
        return "group: " + this.group;
    }
}

これを使うコードを見てみましょう。

package com.example.config;

import org.eclipse.microprofile.config.ConfigProvider;

public class Main {
    public static void main(String[] args) {
        var config = ConfigProvider.getConfig();
        var value = config.getValue("com.example.dog", Dogs.class);
        System.out.println(value);
    }
}

このコードを動かす際には、システムプロパティに -Dcom.example.dog=Hound を設定してみましょう。

実行結果として以下のように標準出力されるはずです。

group: Hound

of メソッドがファクトリとして利用されていますね。

まとめ

このエントリでは、MicroProfile Configを使った設定管理の方法について説明しました。

MicroProfile Configを使うと、設定値の変更が及ぼす影響を最小限に抑えつつ、柔軟な設定管理が実現できます。設定値を文字列から任意の型に自動変換できるので設定ミスによる深刻な動作不良を防げるでしょう。高い可用性を求められるアプリケーションでも設定ソースを作りこむことで対応できます。
規約に基づいてに実行される型変換では文字列を引数にとるコンストラクタやファクトリメソッドがあれば、それが使われます。独自の型を使いたい場合にも簡単に対応できます。

MicroProfile Configをうまく活用してアプリケーションの品質向上に役立ててください。

執筆:@sato.taichi
Shodoで執筆されました