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

OKD/Minishift上で、マルチモジュール構成のMavenプロジェクトをデプロイする

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

  • JavaアプリケーションをMavenのマルチモジュール構成で作った時に、「どうやってデプロイするんだっけ?」と思ったので
  • S2Iビルドする時に、なにか設定とかがいるのではと思って調べてみようと

というわけで、OKD/Minishift上にマルチモジュール構成のMavenプロジェクトをデプロイしてみます。

お題

OKD/Minishift上に、ごくごく簡単な2つのMavenプロジェクトをデプロイしてみます。

2つというのは、

という感じで。

ほぼ答えが書いていますが、参考にしたのはこちらのエントリ。

Maven Multi-Module Projects and OpenShift – OpenShift Blog

環境

今回の環境は、こちら。

$ minishift version
minishift v1.24.0+8a904d0


$ oc version
oc v3.10.0+dd10d17
kubernetes v1.10.0+b81c8f8
features: Basic-Auth GSSAPI Kerberos SPNEGO

Server https://192.168.42.24:8443
openshift v3.10.0+e3465d0-44
kubernetes v1.10.0+b81c8f8

マルチモジュール構成のWARなアプリケーションをデプロイする

それでは、最初はWARファイルから。

こんな感じのMavenプロジェクトを用意。

$ find pom.xml library web -type f
pom.xml
library/pom.xml
library/src/main/resources/META-INF/beans.xml
library/src/main/java/org/littlewings/openshift/MessageService.java
web/web.iml
web/src/main/java/org/littlewings/openshift/JaxrsActivator.java
web/src/main/java/org/littlewings/openshift/MessageResource.java

「web」がWARを構成するプロジェクトで、JAX-RSに関連するクラスを置いています。「library」は「web」から
参照されるプロジェクトで、CDI管理Beanを置いています。

内容は、こんな感じ。

library

pom.xml(の一部)
※依存関係がちょっと雑です

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

CDI管理Bean。
library/src/main/java/org/littlewings/openshift/MessageService.java

package org.littlewings.openshift;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageService {
    public String get() {
        return "Hello World!!";
    }
}

CDI有効化のための、beans.xml。
library/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_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>

web

WARファイル側。

pom.xml(の抜粋)。

    <artifactId>web</artifactId>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

    <dependencies>
        <dependency>
            <groupId>xxx.yyy</groupId>
            <artifactId>library</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
    </build>

JAX-RS関連のクラス。
リソースクラス。
web/src/main/java/org/littlewings/openshift/MessageResource.java

package org.littlewings.openshift;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@ApplicationScoped
@Path("message")
public class MessageResource {
    @Inject
    MessageService messageService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String get() {
        return messageService.get();
    }
}

JAX-RSの有効化。
web/src/main/java/org/littlewings/openshift/JaxrsActivator.java

package org.littlewings.openshift;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}
トップレベルのプロジェクト

トップレベルのプロジェクトは、pom.xmlにモジュールのリストがあるだけです。
pom.xml(の抜粋)。

    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>web</module>
    </modules>

このプロジェクトを「http://〜/maven-multi-project-war.git」という感じでGitリポジトリに登録しておきます。

デプロイする

では、このWebアプリケーションをOKDにデプロイしてみます。

このようなマルチモジュールな構成のMavenプロジェクトをデプロイするには、環境変数「ARTIFACT_DIR」をビルド時に
指定します。

ビルド時に指定するので、いきなり「oc new-app」ではなく、「oc new-build」で始めてみます。

$ oc new-build openshift/wildfly:12.0~http://〜/maven-multi-project-war.git -e ARTIFACT_DIR=web/target

ImageStreamとしてWildfFlyを指定し、環境変数「ARTIFACT_DIR」にはwebモジュールのtargetディレクトリ、
つまり最終的に使われるアーティファクトが生成されるディレクトリを指定します。

環境変数「ARTIFACT_DIR」で適切なディレクトリを指定しなかった場合は、この後にDeploymentConfigを
作成してデプロイした時に、WildFlyになにもデプロイされません…。

ビルドが終わったら、「oc new-app」を実行してデプロイして、Routeもexposeします。

$ oc new-app maven-multi-project-war
$ oc expose svc/maven-multi-project-war

確認。

$ curl maven-multi-project-war-myproject.xxx.xxx.xx.xx.nip.io/message
Hello World!!

OKです。

ちなみに、ビルドするモジュールを絞りたい場合は、環境変数「MAVEN_ARGS_APPEND」を併用します。

$ oc new-build openshift/wildfly:12.0~http://〜/maven-multi-project-war.git -e ARTIFACT_DIR=web/target -e MAVEN_ARGS_APPEND='-pl web -am'

これで、Maven実行時には以下のようなコマンドになります。

$ mvn package -Popenshift -DskipTests -B -s /opt/app-root/src/.m2/settings.xml -pl web -am

ビルドしたいモジュールと、関連するモジュールがビルドされる感じになりますね。

YAMLで書くと

今回のBuildConfigをYAMLで書いた場合の、該当の箇所を抜粋すると、こうですね。

apiVersion: v1
kind: BuildConfig

## 省略...

spec:

## 省略...

  strategy:
    sourceStrategy:
      env:
      - name: ARTIFACT_DIR
        value: web/target
      - name: MAVEN_ARGS_APPEND
        value: -pl web -am

## 省略...

という感じになります。

マルチモジュール構成のUber JARなアプリケーションをデプロイする

続いて、マルチモジュール構成なUber JARなプロジェクトをデプロイします。

こちらは、こんな構成。

$ find pom.xml library launcher -type f
pom.xml
library/pom.xml
library/src/main/java/org/littlewings/openshift/MessageService.java
launcher/pom.xml
launcher/src/main/java/org/littlewings/openshift/App.java

あらシンプル。内容は、Spring BootアプリケーションでUber JARになる方が「launcher」モジュールです。

OpenJDKのImageStreamを入れる

中身に入る前に、OKDにOpenJDKのImageStreamを突っ込んでおきます。

$ oc create -f https://raw.githubusercontent.com/jboss-openshift/application-templates/ose-v1.4.15/openjdk/openjdk18-image-stream.json -n openshift --as system:admin

Uber JARの実行には、こちらを使用します。

Minishiftの、「admin-user」addonは有効にしています。

$ minishift addon list
- admin-user             : enabled  P(0)
- anyuid             : disabled P(0)
- che                : disabled P(0)
- htpasswd-identity-provider     : disabled P(0)
- registry-route         : disabled P(0)
- xpaas              : disabled P(0)

では、中身の方へ。

library

pom.xml(の一部)。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

いやぁ、こちらも依存関係が雑です…。

Serviceクラス。
library/src/main/java/org/littlewings/openshift/MessageService.java

package org.littlewings.openshift;

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class MessageService {
    public Mono<String> get() {
        return Mono.just("Hello World!!");
    }
}
launcher

lancher側。
pom.xml(の一部)。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>xxx.yyy</groupId>
            <artifactId>library</artifactId>
            <version>${project.version}</version>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.0.5.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

起動クラス兼RestController。
launcher/src/main/java/org/littlewings/openshift/App.java

package org.littlewings.openshift;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@SpringBootApplication
@RestController
public class App {
    MessageService messageService;

    public App(MessageService messageService) {
        this.messageService = messageService;
    }

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

    @GetMapping("message")
    public Mono<String> message() {
        return messageService.get();
    }
}
トップレベルのプロジェクト

トップレベルのプロジェクトは、pom.xmlにモジュールのリストがあるだけです。
pom.xml(の一部)。

    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>launcher</module>
    </modules>

WARプロジェクトの時と、同じようなものですね。

このプロジェクトを「http://〜/maven-multi-project-jar.git」という感じでGitリポジトリに登録しておきます。

デプロイする

では、このUber JARなアプリケーションをOKDにデプロイしてみます。

といっても、ここから先の指定は、先程のWebアプリケーションの時と似たようなものです。

「oc new-build」で、環境変数「ARTIFACT_DIR」を「launcher/target」ディレクトリに指定します。

$ oc new-build openshift/redhat-openjdk18-openshift:1.4~http://〜/maven-multi-project-jar.git -e ARTIFACT_DIR=launcher/target

ビルドが終わったら、DeploymentConfigとRouteの作成。

$ oc new-app maven-multi-project-jar
$ oc expose svc/maven-multi-project-jar

確認。

$ curl maven-multi-project-jar-myproject.xxx.xxx.xx.xx.nip.io/message
Hello World!!

OKですね。

もちろん、「oc new-build」時に環境変数「MAVEN_ARGS_APPEND」を指定できるのも同じです。

$ oc new-build openshift/redhat-openjdk18-openshift:1.4~http://〜/maven-multi-project-jar.git -e ARTIFACT_DIR=launcher/target -e MAVEN_ARGS_APPEND='-pl launcher -am'

実行されるMavenのコマンドは、このようになりました。

$ mvn -Dmaven.repo.local=/tmp/artifacts/m2 -s /tmp/artifacts/configuration/settings.xml -e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga -Dfabric8.skip=true package --batch-mode -Djava.net.preferIPv4Stack=true -pl launcher -am

ちなみに、このようなマルチモジュール構成で環境変数「ARTIFACT_DIR」を指定しなかった場合は、
デプロイ対象が見つからずにエラーになります。

Copying Maven artifacts from /tmp/src/target to /deployments ...
Running: cp *.jar /deployments
/usr/local/s2i/assemble: line 71: cd: /tmp/src/target: No such file or directory
cp: cannot stat '*.jar': No such file or directory
Aborting due to error code 1 for copying artifacts from /tmp/src/target to /deployments
error: build error: non-zero (13) exit code from registry.access.redhat.com/redhat-openjdk-18/openjdk18-openshift@sha256:dc84fed0f6f40975a2277c126438c8aa15c70eeac75981dbaa4b6b853eff61a6

ここは、WARの時とは異なりますね。といっても、WARの時はデプロイが空振りするので、結局それでは
ダメなのですけどね。

YAMLで書くと

今回のBuildConfigをYAMLで書いた場合の、該当の箇所を抜粋すると、こうですね。

apiVersion: v1
kind: BuildConfig
metadata:

## 省略...

spec:

## 省略...

  strategy:
    sourceStrategy:
      env:
      - name: ARTIFACT_DIR
        value: launcher/target
      - name: MAVEN_ARGS_APPEND
        value: -pl launcher -am

## 省略...

オマケ

ビルドのこういった環境変数を扱っている箇所を、ちょっと探しておきました。

WildFly
ARTIFACT_DIR
https://github.com/openshift-s2i/s2i-wildfly/blob/720a8d33b6af1cdc39d28333d44df7adb6bcda9b/12.0/s2i/bin/assemble#L220

MAVEN_ARGS_APPEND(とMAVEN_ARGS)
https://github.com/openshift-s2i/s2i-wildfly/blob/720a8d33b6af1cdc39d28333d44df7adb6bcda9b/12.0/s2i/bin/assemble#L255-L277

S2Iのスクリプトを確認。

$ oc get istag/wildfly:12.0 -n openshift -o yaml

YAMLを抜粋。

apiVersion: image.openshift.io/v1
generation: 2
image:
  dockerImageLayers:

## 省略...

  dockerImageMetadata:

## 省略...

    Config:
      Cmd:
      - /bin/sh
      - -c
      - $STI_SCRIPTS_PATH/usage
      Entrypoint:
      - container-entrypoint

container-entrypointを見よ、と。

「oc rsh」か「oc debug」で確認。

$ oc rsh dc/maven-multi-project-war
## または
$ oc debug dc/maven-multi-project-war

で、「container-entrypointo」は、と。

sh-4.2$ which container-entrypoint
/usr/bin/container-entrypoint

sh-4.2$ cat $(which container-entrypoint)
#!/bin/bash
exec "$@"

うん、わからん…。

Dockerfileを見てみます。 https://github.com/openshift-s2i/s2i-wildfly/blob/master/12.0/Dockerfile

「STI_SCRIPTS_PATH」を見ればよいみたいです。

# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH
COPY ./s2i/bin/ $STI_SCRIPTS_PATH

そういえば、さっきのYAMLにも書いてありましたね。

    Config:
      Cmd:
      - /bin/sh
      - -c
      - $STI_SCRIPTS_PATH/usage
      Entrypoint:
      - container-entrypoint

「/usr/libexec/s2i」らしいです。

sh-4.2$ env | grep STI_SCRIPTS_PATH
STI_SCRIPTS_PATH=/usr/libexec/s2i

では、assembleを確認。

sh-4.2$ view $STI_SCRIPTS_PATH/assemble

「MAVEN_ARGS_APPEND」や

  # Append user provided args
  if [ -n "$MAVEN_ARGS_APPEND" ]; then
    export MAVEN_ARGS="$MAVEN_ARGS $MAVEN_ARGS_APPEND"
  fi

「ARTIFACT_DIR」を見てみたり。

# the subdirectory within LOCAL_SOURCE_DIR from where we should copy build
# artifacts (*.war, *.jar)
ARTIFACT_DIR=${ARTIFACT_DIR:-target}

OpenJDK
S2Iのスクリプトを確認。

$ oc get istag/redhat-openjdk18-openshift:1.4 -o yaml

YAMLの抜粋。

apiVersion: image.openshift.io/v1
generation: 2
image:
  dockerImageLayers:

## 省略...

  dockerImageManifestMediaType: application/vnd.docker.distribution.manifest.v2+json
  dockerImageMetadata:
    Architecture: amd64
    Config:
      Cmd:
      - /usr/local/s2i/run
      Env:
      - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/s2i

「oc rsh」、もしくは「oc debug」でコンテナ内に入って、確認。

$ oc rsh dc/maven-multi-project-jar
## もしくは
$ oc debug dc/maven-multi-project-jar

$ sh-4.2$ view /usr/local/s2i/assemble

スクリプトの一部を見てみます。

sh-4.2$ view /usr/local/s2i/assemble

こんな感じ。

function build_maven() {
  # Where artifacts are created during build
  local build_dir=$1

  # Where to put the artifacts
  local app_dir=$2

  local jvm_option_file=/opt/run-java/java-default-options
  if [ -z "${MAVEN_OPTS}" -a -x "$jvm_option_file" ] ; then
    export MAVEN_OPTS="$($jvm_option_file)"
    echo "Setting MAVEN_OPTS to ${MAVEN_OPTS}"
  fi
  # Default args: no tests, if a module is specified, only build this module
  local maven_args=${MAVEN_ARGS:--e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga -Dfabric8.skip=true package}

  # Use batch mode (CLOUD-579)
  echo "Found pom.xml ... "
  local mvn_cmd="${maven_env_args} ${maven_args} --batch-mode -Djava.net.preferIPv4Stack=true ${MAVEN_ARGS_APPEND}"
  echo "Running 'mvn ${mvn_cmd}'"