CLOVER🍀

That was when it all began.

Configuration for MicroProfileを試す

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

  • 長らくJava EEになかったConfig系のAPI(JNDI…)が、MicroProfileに入っているということで1度試しておこうと
  • Apache DeltaSpikeにインスパイアされているようなので、そちらを知っていれば敷居もそれほど高くないかも?

ということで、Configuration for MicroProfileを試してみました。

過去に書いた、Apache DeltaSpikeのConfigurationについてのエントリは、こちら。

Apache DeltaSpikeのConfiguration Mechanismを試す - CLOVER🍀

Configuration for MicroProfile?

Configuration for MicroProfileについてのAPIや仕様書があるのは、こちら。

GitHub - eclipse/microprofile-config: MicroProfile Configuration Feature

現在のバージョンは、1.3です。

https://github.com/eclipse/microprofile-config/releases/tag/1.3

仕様書。

https://github.com/eclipse/microprofile-config/releases/download/1.3/microprofile-config-spec-1.3.pdf

仕様書内に、「Docker」という具体的な単語が出てくるところに、ちょっと不思議な感じがします。

Microprofile-Config provides a way to achieve this goal by aggregating configuration from many different ConfigSources and presents a single merged view to the user. This allows the application to bundle default configuration within the application. It also allows to override the defaults from outside, e.g. via an environment variable a Java system property or via Docker.

MicroProfileにおける、設定に関するAPIです。

以下あたりがポイントでしょうか?

  • Configを起点に、単独で使える
  • プロパティファイル以外にも、任意のプロパティソースから設定を取得するように構成できる
  • 環境変数システムプロパティによる、値の上書きが可能
  • CDIとの連携

MicroProfile Config APIとも呼ばれる?

MicroProfile Config を使用して単一の API から構成オプションを使用可能にする

このIBMのページは、日本語情報としてかなり詳しく書かれているので、とても参考になりますね。

実装は、同じくGitHub上に書かれているのですが以下あたりがあるようです。

今回は、スタンドアロンでも使えそうなApache Geronimo Configを使って、Configuration for MicroProfileを
試してみようと思います。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-0ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


$ mvn -version
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T03:33:14+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-34-generic", arch: "amd64", family: "unix"

準備

Apache Geronimo Configを使うための依存関係は、こちら。

        <dependency>
            <groupId>org.apache.geronimo.config</groupId>
            <artifactId>geronimo-config-impl</artifactId>
            <version>1.2</version>
        </dependency>

アプリケーションサーバー内で使うなどで、APIそのものはコンテナから提供されている場合は、API仕様の方を
providedにします。

        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <version>1.3</version>
            <scope>provided</scope>
        </dependency>

Configuration for MicroProfileでは、設定ファイルを「META-INF/microprofile-config.properties」というパスで
用意するようなので、最初にこちらを作成しておきましょう。

src/main/resources/META-INF/microprofile-config.properties

厳密にはなくてもいいのですが、まあ、設定ファイルは作るでしょう。

初めてのConfiguration for MicroProfile

まずは、以下のコードを雛形にして進めていきます。
src/main/java/org/littlewings/microprofile/config/App.java

package org.littlewings.microprofile.config;

import java.util.List;
import java.util.NoSuchElementException;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;

public class App {
    public static void main(String... args) {

        // ここに、Configuration for MicroProfileを使ったコードを書く
    }
}
Key API

Configuration for MicroProfileを使うために覚えておいた方がよいAPIは、まずは

  • Config
  • ConfigProvider

ですね。ConfigProviderは、Configを取得する起点となります。

CDI環境下で使う場合は、加えて「@ConfigProperty」アノテーションを使います。

拡張する場合は

  • ConfigSource
  • ConfigSourceProvider
  • Converter

を使うことになるでしょう。

というわけで、まずはConfigを取得します。

        Config config = ConfigProvider.getConfig();

※Builderを使った構築方法もあるのですが、今回はパス…

設定ファイルには、以下のような項目を用意してみました。
src/main/resources/META-INF/microprofile-config.properties

some.project.property.name = My Name
some.project.property.int.value = 150
some.project.property.string.values = foo,bar, hoge
some.project.property.integer.values = 50, 75, 100
some.project.property.class.name = java.lang.String

foo.bar = Test
some.project.property.other.reference = Resolve? = ${foo.bar}

どう動くかは、まだ書いていませんよ?

最初の値を取得。

some.project.property.name = My Name

コード。

        System.out.printf("some.project.property.name = %s%n", config.getValue("some.project.property.name", String.class));

Config#getValueで、プロパティ名と型を指定して呼び出します。

config.getValue("some.project.property.name", String.class);

結果。

some.project.property.name = My Name

プロパティファイルに存在しないプロパティを指定してみましょう。

        try {
            config.getValue("missing.property", String.class);
        } catch (NoSuchElementException e) {
            System.out.println("missing property = " + e.getClass().getName() + ": " + e.getMessage());
        }

この場合、NoSuchElementExceptionがスローされ、そんなプロパティないよ、と言われます。

missing property = java.util.NoSuchElementException: No configured value found for config key missing.property

プロパティがないかもしれない場合は、Config#getOptionalValueを使うとよいでしょう。

        System.out.printf("missing property = %s%n",
                config.getOptionalValue("missing.property", String.class).orElse("default-value"));

型変換する

Configuration for MicroProfileには、型変換の仕組みがあり(Converterといいます)、取得したプロパティに対する
型変換を行うことができます。

ビルトインとしては、仕様書を見ると

  • Boolean
  • int and Integer
  • long and Long
  • float and Float
  • double and Double
  • Class(Class.forName)

がサポートされているようです。

Apache Geronimo Configを見ると、URLもサポートされていそうな雰囲気ですね。

https://github.com/apache/geronimo-config/tree/geronimo-config-1.2/impl/src/main/java/org/apache/geronimo/config/converters

あと、Configuration for MicroProfileの仕様としては配列に対する変換が入っていたり、変換先の型にstaticメソッドで

  • of
  • valueOf
  • parse

※いずれも、String or CharSequenceを引数に取る

があれば、自動変換してくれるそうな。これは仕様書に書かれていて、Apache Geronimo Configの実装でいくと
このあたりになります。

https://github.com/apache/geronimo-config/blob/geronimo-config-1.2/impl/src/main/java/org/apache/geronimo/config/converters/ImplicitConverter.java#L36-L64

今回は、Converterの自作は行いません。Apache Geronimo Configの実装例などを見つつ

https://github.com/apache/geronimo-config/blob/geronimo-config-1.2/impl/src/main/java/org/apache/geronimo/config/converters/IntegerConverter.java

作成したら「META-INF/services/org.eclipse.microprofile.config.spi.Converter」ファイルに、クラスのFQCNを定義すると
独自のConverterが使えるようになります。

…前置きが長くなりました。コードに移りましょう。

例えば、プロパティをintとして取得します。

some.project.property.int.value = 150

コード。

        int intValue = config.getValue("some.project.property.int.value", Integer.class);
        System.out.printf("some.project.property.int.value = %d%n", intValue);

結果。

some.project.property.int.value = 150

int(Integer)に変換して取得できています。

次は、配列。「,」区切りのプロパティは、配列として取得することができます。空白は、読み飛ばしてくれるっぽい…。

some.project.property.integer.values = 50, 75, 100

コード。

        String[] asArrayValue = config.getValue("some.project.property.string.values", String[].class);
        System.out.printf(
                "some.project.property.string.values: [0] = %s, [1] = %s, [2] = %s%n",
                asArrayValue[0],
                asArrayValue[1],
                asArrayValue[2]
        );

結果。

some.project.property.string.values: [0] = foo, [1] = bar, [2] = hoge

ちなみにですね、Listにして取得することはできなかったりします。

        try {
            List<String> asListValue = config.getValue("some.project.property.string.values", List.class);
        } catch (IllegalArgumentException e) {
            System.out.printf("list not supported = %s%n", e.getMessage());
        }

Converterがないよ、って言われます。

list not supported = No Converter registered for class interface java.util.List

Integer配列への変換も可能です。

some.project.property.integer.values = 50, 75, 100

コード。

        Integer[] asIntegerArrayValue = config.getValue("some.project.property.integer.values", Integer[].class);
        System.out.printf(
                "some.project.property.integer.values: [0] = %d, [1] = %d, [2] = %d%n",
                asIntegerArrayValue[0],
                asIntegerArrayValue[1],
                asIntegerArrayValue[2]
        );

配列の中身に対するConverterは、単独の値に対するConverterが使われるので、すでにあるConverterを使って
配列に変換することができます。

Classクラスへの変換。

some.project.property.class.name = java.lang.String

コード。

        //Class<String> clazz = config.getValue("some.project.property.class.name", Class.class);
        Class<?> clazz = config.getValue("some.project.property.class.name", Class.class);
        System.out.printf("some.project.property.class.name = %s%n", clazz.getName());

結果。

some.project.property.class.name = java.lang.String

他のプロパティを参照できる?

SpringやApache DeltaSpikeでは、プロパティファイルなどで定義した値を、別のプロパティから参照することが
可能でした。

こういうやつですね。

foo.bar = Test
some.project.property.other.reference = Resolve? = ${foo.bar}

Configuration for MicroProfileではどうでしょう?

System.out.printf("some.project.property.other.reference = %s%n", config.getValue("some.project.property.other.reference", String.class));

答えは、「参照できない」です。

some.project.property.other.reference = Resolve? = ${foo.bar}

そのままの値が出ます。まあ、仕様書にもそういった記述はありませんから。

別のプロパティソースからも設定を読み込むように構成する

ここまで、「META-INF/microprofile-config.properties」という単一のファイルに、プロパティを定義してきました。
ですが、Configuration for MicroProfileでは、それ以外のソースからプロパティを取得するように拡張することができます。

ここで使うのが、ConfigSourceおよびConfigSourceProviderです。

ConfigSource

仕様書のサンプルでは、データベースから読み込むような雰囲気のものが登場します。

ここでは、新しくプロパティファイルを追加して読み込むようにしましょう。こんな感じに、クラスパス上から
プロパティファイルを読むようなConfigSourceを作成。 src/main/java/org/littlewings/microprofile/config/MyCustomConfigSource.java

package org.littlewings.microprofile.config;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.eclipse.microprofile.config.spi.ConfigSource;

public class MyCustomConfigSource implements ConfigSource {
    Map<String, String> properties;

    public MyCustomConfigSource() {
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("META-INF/my-custom-config.properties")) {
            Properties props = new Properties();
            props.load(is);
            properties = new HashMap<>((Map) props);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Map<String, String> getProperties() {
        return properties;
    }

    @Override
    public String getValue(String propertyName) {
        return properties.get(propertyName);
    }

    @Override
    public String getName() {
        return "my-constom-config";
    }
}

これを、「META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource」ファイルを作成して、クラスの
FQCNを記載します。 src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource

org.littlewings.microprofile.config.MyCustomConfigSource

プロパティファイルも作成。 src/main/resources/META-INF/my-custom-config.properties

some.project.custom.property.name = My Custom Config

これで、先程使っていたConfigから、追加したプロパティファイルの値も取得できるようになります。

        System.out.printf("some.project.custom.property.name = %s%n",
                config.getValue("some.project.custom.property.name", String.class));

結果。

some.project.custom.property.name = My Custom Config

各ConfigSourceを統合して、ひとつのConfigとして見せてくれます、と。

優先度

ConfigSourceには優先度の概念があり、「config_ordinal」で指定することができます。

例えば、先程新しく追加したプロパティファイルに、以下のように記載すると優先度は「120」となります。

config_ordinal = 120
some.project.custom.property.name = My Custom Config

デフォルトの「META-INF/microprofile-config.properties」は、「100」なので、これより優先度を高くすることができる、
ということになります。

また、ConfigSourceインターフェースのデフォルトメソッドをオーバーライドすることで、設定することもできます。

public class MyCustomConfigSource implements ConfigSource {

    @Override
    public int getOrdinal() {
        return 120;
    }

このあと、この優先度の話は少し出てくるので、覚えておくとよいです。

ConfigSourceProvider

と、このサンプルを出すと、「プロパティファイルを追加する度に、ConfigSourceを作成することになる」と受け取るかも
しれませんが、ConfigSourceProviderを使うと複数のConfigSourceを取り回すことができるようになります。

例えば、プロパティファイルから読み込むような汎用的なConfigSourceを作成して src/main/java/org/littlewings/microprofile/config/GenericCustomConfigSource.java

package org.littlewings.microprofile.config;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.eclipse.microprofile.config.spi.ConfigSource;

public class GenericCustomConfigSource implements ConfigSource {
    String name;
    Map<String, String> properties;

    public GenericCustomConfigSource(URL propertiesFileUrl) {
        name = propertiesFileUrl.toExternalForm();
        this.properties = loadProperties(propertiesFileUrl);
    }

    @Override
    public Map<String, String> getProperties() {
        return properties;
    }

    @Override
    public String getValue(String propertyName) {
        return properties.get(propertyName);
    }

    @Override
    public String getName() {
        return name;
    }

    Map<String, String> loadProperties(URL propertiesFileUrl) {
        Properties props = new Properties();

        try (InputStream is = propertiesFileUrl.openStream()) {
            props.load(is);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        return new HashMap<>((Map) props);
    }
}

これを使用する、ConfigSourceProviderを作成します。
src/main/java/org/littlewings/microprofile/config/PropertiesFileConfigProvider.java

package org.littlewings.microprofile.config;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;

public class PropertiesFileConfigProvider implements ConfigSourceProvider {
    @Override
    public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
        List<ConfigSource> configSources = new ArrayList<>();

        String[] propertiesFilePaths = {
                "META-INF/my-custom-config.properties"
        };

        try {
            for (String path : propertiesFilePaths) {
                Collections
                        .list(forClassLoader.getResources(path))
                        .forEach(url -> configSources.add(new GenericCustomConfigSource(url)));
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        return configSources;
    }
}

プロパティファイルには、先程作成したものを使用しました。

        String[] propertiesFilePaths = {
                "META-INF/my-custom-config.properties"
        };

ConfigSourceProvider#getConfigSourcesで、複数のConfigSourceを作成するようにすれば、ファイルごとにConfigSourceを
作成するような手間は低減することができます(最初のひとつは必要ですが)。

作成したConfigSourceProviderを使用するには、「META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider」内に
作成したクラスのFQCNを記載します。
src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider

org.littlewings.microprofile.config.PropertiesFileConfigProvider

設定値を環境変数システムプロパティで上書きする

Configuration for MicroProfileでは、優先度によるConfigSourceの上書きを行うことができます。

ConfigSourceとして、環境変数システムプロパティ用のものが備わっており、それぞれのconfig_ordinalが

となっています。

ですので、例えば最初に定義したプロパティ

some.project.property.name = My Name

環境変数で定義すると

SOME_PROJECT_PROPERTY_NAME='My Name from Env'

値が上書きされます。

some.project.property.name = My Name from Env

環境変数で指定する場合は、英数字および「_」以外の文字は「_」に変換され、かつUpperCaseをかけて参照することが
仕様書として書かれています。

さらにシステムプロパティを使用すると

-Dsome.project.property.name='My Name from System Property'

こちらも上書きされます。

some.project.property.name = My Name from System Property

環境変数システムプロパティの両方を指定した場合は、config_ordinalに従いシステムプロパティの方が優先されます。

つまり、自分でConfigSourceなどを作成する場合は、config_ordinalを300を下回るように作っておいた方が
無難な感じですね?

Apache Geronimo Configでは、これらの環境変数システムプロパティを扱う仕組みもConfigSourceとして
実装されています。

https://github.com/apache/geronimo-config/blob/geronimo-config-1.2/impl/src/main/java/org/apache/geronimo/config/configsource/SystemEnvConfigSource.java

https://github.com/apache/geronimo-config/blob/geronimo-config-1.2/impl/src/main/java/org/apache/geronimo/config/configsource/SystemPropertyConfigSource.java

CDIと統合する

最後、CDIと統合してみます。

CDIの実装としては、Weld SEを使用しました。

        <dependency>
            <groupId>org.jboss.weld.se</groupId>
            <artifactId>weld-se-core</artifactId>
            <version>3.0.5.Final</version>
        </dependency>

CDI有効化のために、beans.xmlを用意します。
src/main/resources/META-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
       bean-discovery-mode="annotated" version="2.0">
</beans>

CDIと統合すると、CDI管理Beanに@ConfigPropertyとしてインジェクションできるようになります。 src/main/java/org/littlewings/microprofile/config/cdi/CdiBeanConfig.java

package org.littlewings.microprofile.config.cdi;

import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class CdiBeanConfig {
    @Inject
    @ConfigProperty(name = "some.project.property.name")
    private String name;

    @Inject
    @ConfigProperty(name = "missing.property", defaultValue = "default-value")
    private String missingProperty;

    @Inject
    @ConfigProperty(name = "some.project.property.int.value")
    private int intValue;

    @Inject
    @ConfigProperty(name = "some.project.property.string.values")
    private String[] values;

    @Inject
    @ConfigProperty(name = "some.project.property.string.values")
    private List<String> valuesAsList;

    @Inject
    @ConfigProperty(name = "some.project.property.integer.values")
    private List<Integer> integerValuesAsList;

    @Inject
    @ConfigProperty(name = "some.project.custom.property.name")
    private String customPropertyName;

    public String getName() {
        return name;
    }

    public String getMissingProperty() {
        return missingProperty;
    }

    public int getIntValue() {
        return intValue;
    }

    public String[] getValues() {
        return values;
    }

    public List<String> getValuesAsList() {
        return valuesAsList;
    }

    public List<Integer> getIntegerValuesAsList() {
        return integerValuesAsList;
    }

    public String getCustomPropertyName() {
        return customPropertyName;
    }
}

こんな感じです。

    @Inject
    @ConfigProperty(name = "some.project.property.name")
    private String name;

インジェクションする場合のデフォルト値は、@ConfigPropertyのdefaultValueで指定します。

    @Inject
    @ConfigProperty(name = "missing.property", defaultValue = "default-value")
    private String missingProperty;

また、@ConfigPropertyを使う場合は、配列のみならずListでのインジェクションが可能になります。

    @Inject
    @ConfigProperty(name = "some.project.property.string.values")
    private String[] values;

    @Inject
    @ConfigProperty(name = "some.project.property.string.values")
    private List<String> valuesAsList;

    @Inject
    @ConfigProperty(name = "some.project.property.integer.values")
    private List<Integer> integerValuesAsList;

これは、仕様書にインジェクションモデルだと配列、List、Setをサポートすべきだよ、と書かれているからですね…。

Injection model For the property injection, Array, List and Set should be supported.

@Inject @ConfigProperty(name="myPets") private String[] myArrayPets;
@Inject @ConfigProperty(name="myPets") private List<String> myListPets;
@Inject @ConfigProperty(name="myPets") private Set<String> mySetPets;

別途追加したConfigSourceの値も、インジェクション可能です。

    @Inject
    @ConfigProperty(name = "some.project.custom.property.name")
    private String customPropertyName;

ですが、ConfigSource単位とかでCDI管理Beanは分けた方が扱いやすいでしょうね。

サンプルコード。 src/main/java/org/littlewings/microprofile/config/cdi/CdiApp.java

package org.littlewings.microprofile.config.cdi;

import java.util.List;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;

public class CdiApp {
    public static void main(String... args) {
        SeContainerInitializer initializer = SeContainerInitializer.newInstance();
        try (SeContainer container = initializer.initialize()) {
            CdiBeanConfig config = container.select(CdiBeanConfig.class).get();

            System.out.printf("some.project.property.name = %s%n", config.getName());

            System.out.printf("missing.property = %s%n", config.getMissingProperty());

            System.out.printf("some.project.property.int.value = %d%n", config.getIntValue());

            String[] asArrayValue = config.getValues();
            System.out.printf(
                    "some.project.property.string.values: [0] = %s, [1] = %s, [2] = %s%n",
                    asArrayValue[0],
                    asArrayValue[1],
                    asArrayValue[2]
            );

            List<String> asListValue = config.getValuesAsList();
            System.out.printf(
                    "some.project.property.string.values: get(0) = %s, get(1) = %s, get(2) = %s%n",
                    asListValue.get(0),
                    asListValue.get(1),
                    asListValue.get(2)
            );

            List<Integer> asIntegerListValue = config.getIntegerValuesAsList();
            System.out.printf(
                    "some.project.property.integer.values: get(0) = %d, get(1) = %d, get(2) = %d%n",
                    asIntegerListValue.get(0),
                    asIntegerListValue.get(1),
                    asIntegerListValue.get(2)
            );

            System.out.printf("some.project.custom.property.name = %s%n",
                            config.getCustomPropertyName());
        }
    }
}

Java SE環境でCDIを使い、SeContainer#selectで取得する感じにしていますが

        SeContainerInitializer initializer = SeContainerInitializer.newInstance();
        try (SeContainer container = initializer.initialize()) {
            CdiBeanConfig config = container.select(CdiBeanConfig.class).get();

CDI管理下では、インジェクションして使うことが多いでしょう。

    @Inject
    CdiBeanConfig config;

コードの詳細の説明は省きます。実行結果は、こちら。

some.project.property.name = My Name
missing.property = default-value
some.project.property.int.value = 150
some.project.property.string.values: [0] = foo, [1] = bar, [2] = hoge
some.project.property.string.values: get(0) = foo, get(1) = bar, get(2) = hoge
some.project.property.integer.values: get(0) = 50, get(1) = 75, get(2) = 100
some.project.custom.property.name = My Custom Config

ProjectStageは?

ところで、Apache DeltaSpikeにあったProjectStage(SpringでいうProfile)みたいなものはあるのかな?と思う方も
いらっしゃるでしょう。

こちらはConfiguration APIのJSRのissueですが、環境変数システムプロパティで解決する内容だろうという扱いに
なっていそうですね。

Think about how and if to support a ProjectStage · Issue #26 · eclipse/ConfigJSR · GitHub