これは、なにをしたくて書いたもの?
Quarkusを使った場合に、アプリケーションの設定をどう扱うのかをちょっと見てみようと。
結論から言うと、Configuration for MicroProfileを使用します。Quarkus上でConfiguration for MicroProfileを使う場合には、どんな感じに
なるのかをちょっと見てみましょう。
Configuration for MicroProfile
Configuration for MicroProfileとは、文字通りMicroProfileでのアプリケーションの設定に関するAPIを定めたものです。
QuarkusにおけるConfiguration for MicroProfileの実装は、こちらのライブラリです。
GitHub - smallrye/smallrye-config: SmallRye implementation of Eclipse MicroProfile Config
今回利用するQuarkus 0.14.0でのConfiguration for MicroProfileのバージョンは、1.3のようです。
ちなみに、Configuration for MicroProfileについては、以前にエントリを書いたことがあります。
Configuration for MicroProfileを試す - CLOVER🍀
この時は、Apache Geronimo Configを使用しました。
今回は、ちょっと簡単にQuarkusで試してみるとしましょう。
環境
今回の環境は、こちら。
$ java -version openjdk version "1.8.0_191" OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.18.04.1-b12) OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode) $ mvn -version Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_191, 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-48-generic", arch: "amd64", family: "unix"
GraalVMも用意。
$ $GRAALVM_HOME/bin/java -version openjdk version "1.8.0_202" OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08) OpenJDK GraalVM CE 1.0.0-rc16 (build 25.202-b08-jvmci-0.59, mixed mode)
簡単なサンプルアプリケーションの作成
それでは、最初にQuarkusのプロジェクトを作成します。
$ mvn io.quarkus:quarkus-maven-plugin:0.14.0:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=quarkus-configuration \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions=resteasy-jsonb
RESTEasy JSONBもちょっと有効にしておきました。
Quarkusでの設定ファイルは、デフォルトで「application.properties」のようです。プロジェクト作成時に、中身のないファイルが
できているので、こちらに追記しましょう。
src/main/resources/application.properties
# Configuration file # key = value sample.greeting.message = Hello Quarkus!!
設定ファイルに追加した項目を利用する、設定用のクラスを作成します。
src/main/java/org/littlewings/quarkus/configuration/MyConfiguration.java
package org.littlewings.quarkus.configuration; import javax.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.inject.ConfigProperty; @ApplicationScoped public class MyConfiguration { @ConfigProperty(name = "sample.greeting.message") String greetingMessage; @ConfigProperty(name = "sample.int.value", defaultValue = "10") int intValue; public String getGreetingMessage() { return greetingMessage; } public int getIntValue() { return intValue; } }
@ConfigPropertyは、Configuration for MicroProfileのアノテーションです。ちなみに、ひとつ設定ファイルに記載していない項目が
ありますが、こちらにはデフォルト値を設定しています。
では、こちらを使用するJAX-RSリソースクラスを作成してみましょう。
src/main/java/org/littlewings/quarkus/configuration/SampleResource.java
package org.littlewings.quarkus.configuration; import java.util.Properties; 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; import org.eclipse.microprofile.config.Config; @Path("sample") public class SampleResource { @Inject MyConfiguration myConfiguration; @Inject Config config; @GET @Path("greeting") @Produces(MediaType.TEXT_PLAIN) public String greeting() { return myConfiguration.getGreetingMessage(); } @GET @Path("int-value") @Produces(MediaType.TEXT_PLAIN) public int intValue() { return myConfiguration.getIntValue(); } @GET @Path("greeting-by-lookup") @Produces(MediaType.TEXT_PLAIN) public String greetingByLookup() { return config.getValue("sample.greeting.message", String.class); } @GET @Path("system-properties") @Produces(MediaType.APPLICATION_JSON) public Properties systemProperties() { return System.getProperties(); } }
使い方の例として、作成した設定用のクラスをインジェクションして利用する方法と
@Inject MyConfiguration myConfiguration; @GET @Path("greeting") @Produces(MediaType.TEXT_PLAIN) public String greeting() { return myConfiguration.getGreetingMessage(); }
Configuration for MicroProfileが提供するConfigインターフェースをインジェクションして、設定項目をルックアップする方法を書いています。
@Inject Config config; @GET @Path("greeting-by-lookup") @Produces(MediaType.TEXT_PLAIN) public String greetingByLookup() { return config.getValue("sample.greeting.message", String.class); }
確認
では、こちらをビルドして
$ mvn package
確認してみましょう。
$ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar
インジェクションしているものと、Configインターフェースから取得しているもの。
$ curl localhost:8080/sample/greeting Hello Quarkus!! $ curl localhost:8080/sample/greeting-by-lookup Hello Quarkus!!
デフォルト値で動作しているもの。
$ curl localhost:8080/sample/int-value 10
OKですね。
Configuration for MicroProfileは、値をシステムプロパティや環境変数で上書きすることができます。
システムプロパティで上書きする場合。
$ java -Dsample.greeting.message='Hello World!' -Dsample.int.value=15 -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar
確認。
$ curl localhost:8080/sample/greeting Hello World! $ curl localhost:8080/sample/greeting-by-lookup Hello World! $ curl localhost:8080/sample/int-value 15
全部変更されましたね。
環境変数で設定を上書きする場合。
$ export SAMPLE_GREETING_MESSAGE='Hello World!' $ export SAMPLE_INT_VALUE=15 $ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar
上書きした値は、システムプロパティの時と同じなので省略します。
続いて、ネイティブイメージを作成して確認してみましょう。
$ mvn package -Dnative
起動しようとすると…怒られます。
$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner java.util.ServiceConfigurationError: org.eclipse.microprofile.config.spi.ConfigSourceProvider: Provider io.quarkus.runtime.configuration.TemporaryConfigSourceProvider not found at java.util.ServiceLoader.fail(ServiceLoader.java:239) at java.util.ServiceLoader.access$300(ServiceLoader.java:185) at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:372) at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404) at java.util.ServiceLoader$1.next(ServiceLoader.java:480) at java.lang.Iterable.forEach(Iterable.java:74) at io.smallrye.config.SmallRyeConfigBuilder.discoverSources(SmallRyeConfigBuilder.java:80) at io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:190) at io.quarkus.runtime.generated.RunTimeConfig.getRunTimeConfiguration(Unknown Source) at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source) at io.quarkus.runtime.Application.start(Application.java:101) at io.quarkus.runtime.Application.run(Application.java:213) at io.quarkus.runner.GeneratedMain.main(Unknown Source) Exception in thread "main" java.lang.RuntimeException: Failed to start quarkus at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source) at io.quarkus.runtime.Application.start(Application.java:101) at io.quarkus.runtime.Application.run(Application.java:213) at io.quarkus.runner.GeneratedMain.main(Unknown Source) Caused by: java.util.ServiceConfigurationError: org.eclipse.microprofile.config.spi.ConfigSourceProvider: Provider io.quarkus.runtime.configuration.TemporaryConfigSourceProvider not found at java.util.ServiceLoader.fail(ServiceLoader.java:239) at java.util.ServiceLoader.access$300(ServiceLoader.java:185) at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:372) at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404) at java.util.ServiceLoader$1.next(ServiceLoader.java:480) at java.lang.Iterable.forEach(Iterable.java:74) at io.smallrye.config.SmallRyeConfigBuilder.discoverSources(SmallRyeConfigBuilder.java:80) at io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:190) at io.quarkus.runtime.generated.RunTimeConfig.getRunTimeConfiguration(Unknown Source) ... 4 more
どうやら、QuarkusのService Providerで参照している、Configuration for MicroProfileのConfigSourceProviderの実装クラスが
見つけられないようです。
もしかして、リフレクションの情報を登録する必要がある?
というわけで、pom.xmlにGraalVM(Substrate VM)への依存関係を追加して
<dependency> <groupId>com.oracle.substratevm</groupId> <artifactId>svm</artifactId> <version>1.0.0-rc16</version> <scope>provided</scope> </dependency>
対象のクラスの情報を登録します。 src/main/java/org/littlewings/quarkus/configuration/QuarkusConfigurationSubstitutions.java
package org.littlewings.quarkus.configuration; import com.oracle.svm.core.annotate.AutomaticFeature; import io.quarkus.runtime.configuration.TemporaryConfigSourceProvider; import org.graalvm.nativeimage.Feature; import org.graalvm.nativeimage.RuntimeReflection; public class QuarkusConfigurationSubstitutions { } @AutomaticFeature class RuntimeReflectionRegistrationFeature implements Feature { @Override public void beforeAnalysis(Feature.BeforeAnalysisAccess access) { try { RuntimeReflection.register(TemporaryConfigSourceProvider.class); RuntimeReflection.register(TemporaryConfigSourceProvider.class.getDeclaredConstructor()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } }
再度パッケージングして、起動。
$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner
今度は、起動します。確認結果は、JARファイルでの実行の時と同じなので、省略します。
ネイティブイメージでもJARファイルでの実行の時と同様、システムプロパティや
$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner -Dsample.greeting.message='Hello World!' -Dsample.int.value=15 -Dcustom.message='Hello!?'
環境変数で値を上書きすることができます。
$ export SAMPLE_GREETING_MESSAGE='Hello World!' $ export SAMPLE_INT_VALUE=15 $ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner
環境変数はまあいいとして、システムプロパティはこの場合、ただのコマンドライン引数になっているのでは?と思うのですが、
どうなっているのでしょう。ちょっとわかりませんでした…。
独自の設定ファイルを追加する(ネイティブイメージの時は未確認)
Quarkusでは、デフォルトの設定ファイルをapplication.propertiesとしています。
ここで、Configuration for MicroProfileの仕様に沿って、独自の設定項目を追加してみましょう。
こんな設定ファイルを作成。
src/main/resources/my-custom-configuration.properties
custom.message = My Custom Configuration!!
この設定ファイルを読み込む、ConfigSourceProviderの実装を作成します。
src/main/java/org/littlewings/quarkus/configuration/MyCustomConfigSourceProvider.java
package org.littlewings.quarkus.configuration; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Arrays; import java.util.Objects; import java.util.stream.Collectors; import io.smallrye.config.PropertiesConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; public class MyCustomConfigSourceProvider implements ConfigSourceProvider { @Override public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) { return Arrays .asList("my-custom-configuration.properties") .stream() .map(filePath -> forClassLoader.getResource(filePath)) .filter(Objects::nonNull) .map(filePath -> { try { return new PropertiesConfigSource(filePath); } catch (IOException e) { throw new UncheckedIOException(e); } }) .collect(Collectors.toList()); } }
プロパティファイルからのConfigSourceの作成は、SmallRye Configのものを利用しました。
このクラスを、Service Providerの仕組みでロードできるように設定します。
src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider
org.littlewings.quarkus.configuration.MyCustomConfigSourceProvider
先ほど作成した設定クラスに、追加した設定ファイルから取得した項目を追加し
@ApplicationScoped public class MyConfiguration { @ConfigProperty(name = "sample.greeting.message") String greetingMessage; @ConfigProperty(name = "sample.int.value", defaultValue = "10") int intValue; @ConfigProperty(name = "custom.message") String customMessage; public String getGreetingMessage() { return greetingMessage; } public int getIntValue() { return intValue; } public String getCustomMessage() { return customMessage; } }
JAX-RSのエンドポイントも追加します。
@GET @Path("custom-config-message") @Produces(MediaType.TEXT_PLAIN) public String customConfigMessage() { return myConfiguration.getCustomMessage(); }
これで、パッケージングして起動。
$ mvn package $ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar
確認。
$ curl localhost:8080/sample/custom-config-message My Custom Configuration!!
追加した設定項目を認識していることを、確認できました。
追加した設定ファイルに関する項目についても、システムプロパティや環境変数で上書きすることができます。
$ java -Dcustom.message=FooBar -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar
ネイティブイメージでも確認してみましょう。
$ mvn package -Dnative
すると、起動する時に設定のバリデーションに失敗したと怒られます。
$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner javax.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: custom.message at io.quarkus.arc.runtime.ConfigDeploymentTemplate.validateConfigProperties(ConfigDeploymentTemplate.java:48) at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties7.deploy(Unknown Source) at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source) at io.quarkus.runtime.Application.start(Application.java:101) at io.quarkus.runtime.Application.run(Application.java:213) at io.quarkus.runner.GeneratedMain.main(Unknown Source) Exception in thread "main" java.lang.RuntimeException: Failed to start quarkus at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source) at io.quarkus.runtime.Application.start(Application.java:101) at io.quarkus.runtime.Application.run(Application.java:213) at io.quarkus.runner.GeneratedMain.main(Unknown Source) Caused by: javax.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: custom.message at io.quarkus.arc.runtime.ConfigDeploymentTemplate.validateConfigProperties(ConfigDeploymentTemplate.java:48) at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties7.deploy(Unknown Source) ... 4 more
ちなみに、システムプロパティなどで明示的に与えると起動できるようになります。
$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner -Dcustom.message=test 2019-05-11 01:06:33,495 INFO [io.quarkus] (main) Quarkus 0.14.0 started in 0.012s. Listening on: http://[::]:8080 2019-05-11 01:06:33,496 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb]
どうやら、追加した設定ファイルがわかっていないようです。
ちなみに、だいぶ意図的だったのですが、ConfigSourceProviderで設定ファイルが必ずある前提のコードにしてあると、
ネイティブイメージにした時にはファイルが見つけられずにコケることになります…。
public class MyCustomConfigSourceProvider implements ConfigSourceProvider { @Override public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) { return Arrays .asList("my-custom-configuration.properties") .stream() .map(filePath -> forClassLoader.getResource(filePath)) .filter(Objects::nonNull) .map(filePath -> { try { return new PropertiesConfigSource(filePath); } catch (IOException e) { throw new UncheckedIOException(e); } }) .collect(Collectors.toList()); } }
なので、今回のConfigSourceProviderは、「設定ファイルがあったら読み込む」という動作にしています。
ちょっと確認のために、JAX-RSリソースクラスのエンドポイントに、こんなメソッドを足してみます。
@GET @Path("dump-properties") @Produces(MediaType.TEXT_PLAIN) public String dumpProperties() { config.getConfigSources().forEach(cs -> { System.out.println("====== " + cs.getName()); cs.getProperties().forEach((k, v) -> System.out.println(" " + k + " = " + v)); }); return "OK!!"; }
現在認識しているConfigSourceを、全部出力してみようと。
この状態で、ひとまずJARファイルで起動してアクセスしてみます。
$ curl localhost:8080/sample/dump-properties
すると、コンソールにはこんな感じで表示されます。
====== SysPropConfigSource ... ====== EnvConfigSource ... ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=application.properties] sample.greeting.message = Hello Quarkus!! ====== PropertiesConfigSource[source=application.properties] sample.greeting.message = Hello Quarkus!! ====== PropertiesConfigSource[source=jar:file:/path/to/quarkus-configuration/target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar!/my-custom-configuration.properties] custom.message = My Custom Configuration!! ====== null:null:ServletConfigSource ====== null:javax.ws.rs.core.Application:FilterConfigSource ====== null:ServletContextConfigSource resteasy.injector.factory = io.quarkus.resteasy.runtime.QuarkusInjectorFactory resteasy.providers = org.jboss.resteasy.plugins.providers.sse.SseEventOutputProvider,org.jboss.resteasy.plugins.providers.DefaultTextPlain,org.jboss.resteasy.plugins.providers.jsonp.JsonObjectProvider,org.jboss.resteasy.plugins.providers.ReactiveStreamProvider,org.jboss.resteasy.plugins.interceptors.CacheControlFeature,org.jboss.resteasy.plugins.interceptors.ServerContentEncodingAnnotationFeature,org.jboss.resteasy.plugins.providers.CompletionStageProvider,org.jboss.resteasy.plugins.interceptors.ClientContentEncodingAnnotationFeature,org.jboss.resteasy.plugins.providers.DefaultNumberWriter,org.jboss.resteasy.plugins.providers.jsonb.JsonBindingProvider,org.jboss.resteasy.plugins.providers.FileProvider,org.jboss.resteasy.plugins.interceptors.MessageSanitizerContainerResponseFilter,org.jboss.resteasy.plugins.providers.MultiValuedParamConverterProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonArrayProvider,org.jboss.resteasy.plugins.providers.DefaultBooleanWriter,org.jboss.resteasy.plugins.providers.ByteArrayProvider,org.jboss.resteasy.plugins.providers.StringTextStar,org.jboss.resteasy.plugins.providers.StreamingOutputProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonStructureProvider,io.quarkus.resteasy.runtime.RolesFilterRegistrar,org.jboss.resteasy.plugins.providers.ReaderProvider,org.jboss.resteasy.plugins.providers.sse.SseEventSinkInterceptor,org.jboss.resteasy.plugins.providers.DataSourceProvider,org.jboss.resteasy.plugins.providers.FileRangeWriter,org.jboss.resteasy.plugins.providers.InputStreamProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonValueProvider resteasy.scanned.resources = org.littlewings.quarkus.configuration.SampleResource resteasy.use.builtin.providers = false resteasy.servlet.mapping.prefix = / ====== PropertiesConfigSource[source=Default configuration values] quarkus.log.filter."org.xnio.nio".if-starts-with = XNIO NIO Implementation Version quarkus.log.console.darken = 0 sample.greeting.message = Hello Quarkus!! quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with = RESTEASY002225 quarkus.log.filter."org.jboss.threads".if-starts-with = JBoss Threads version quarkus.log.filter."org.xnio".if-starts-with = XNIO version ====== default values
application.propertiesの内容などが、確認できますね。ところで、application.propertiesと同じ内容が入った以下のようなものもあります。
====== PropertiesConfigSource[source=Default configuration values]
ここで、ネイティブイメージで確認すると、ガラッと結果が変わります。
====== SysPropConfigSource ... ====== EnvConfigSource ... ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=application.properties] ====== PropertiesConfigSource[source=Default configuration values] quarkus.log.filter."org.xnio.nio".if-starts-with = XNIO NIO Implementation Version quarkus.log.console.darken = 0 sample.greeting.message = Hello Quarkus!! quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with = RESTEASY002225 quarkus.log.filter."org.jboss.threads".if-starts-with = JBoss Threads version quarkus.log.filter."org.xnio".if-starts-with = XNIO version ====== default values
こちらの方しか認識していませんね。
====== PropertiesConfigSource[source=Default configuration values]
なので、ネイティブイメージの時には実際には異なるファイルから読み込んでいるようなように見えますね。
その元ネタは、ビルド時に生成されるこちらのファイルのようです。 target/wiring-classes/META-INF/quarkus-default-config.properties
#This is the generated set of default configuration values #Sat May 11 01:08:30 JST 2019 quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with=RESTEASY002225 quarkus.log.console.darken=0 sample.greeting.message=Hello Quarkus\!\! quarkus.log.filter."org.xnio".if-starts-with=XNIO version quarkus.log.filter."org.jboss.threads".if-starts-with=JBoss Threads version quarkus.log.filter."org.xnio.nio".if-starts-with=XNIO NIO Implementation Version
ソースコードとしては、このあたりに。
で、この生成されたquarkus-default-config.properties、Build Time Extensionで細工しているような感じに見えます。
CDI REFERENCE / Build Time Extension Points
https://quarkus.io/guides/extension-authors-guide.html
なんかちょっと遠そうなので、今回はこのあたりで。
どこかでBuild Time Extensionを覚えるなりしてから、また戻ってきましょう。
ネイティブイメージにする時には、いろいろあるんですねぇ…。
まとめ
QuarkusでのConfiguration for MicroProfileを試してみました。
JARファイルでの実行でも、ネイティブイメージの実行でも使えますが、追加の設定ファイルを書いたりすると、
ネイティブイメージにした時に困ったことになるような…。
とりあえず、今はapplication.propertiesだけを使っておくのが無難ですかねぇ。