Config MP Guide

This guide describes how to create a sample MicroProfile (MP) project that can be used to run some basic examples using both default and custom configuration with Helidon MP.

What you need

About 30 minutes
Helidon Prerequisites

Getting started with configuration

Helidon provides a very flexible and comprehensive configuration system, offering you many application configuration choices. You can include configuration data from a variety of sources using different formats, like JSON and YAML. Furthermore, you can customize the precedence of sources and make them optional or mandatory. This guide introduces Helidon MP configuration and demonstrates the fundamental concepts using several examples. Refer to Helidon Config for the full configuration concepts documentation.

Create a sample Helidon MP project

Use the Helidon MP Maven archetype to create a simple project that can be used for the examples in this guide.

Run the Maven archetype:
mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=1.4.12 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-mp \
    -Dpackage=io.helidon.examples.quickstart.mp
Copied
The project will be built and run from the helidon-quickstart-mp directory:
cd helidon-quickstart-mp
Copied

Configuration Formats

Helidon configuration sources can use different formats for the configuration data. You can specify the format on a per-source bases, mixing and matching formats as required. Here are the supported formats, each with the extension name you should use. By default, Helidon will determine the media type based on the extension name.

  • Java Property (.properties)

  • JSON (.json)

  • YAML (.yaml)

  • HOCON (.conf)

The remainder of this document will use these formats in examples and show you how to configure Helidon to parse them.

Default configuration

Helidon has an internal configuration, so you are not required to provide any configuration data for your application, though in practice you most likely would. By default, that configuration can be overridden from three sources: system properties, environment variables, and the contents of META-INF/microprofile-config.properties. For example, if you specify a custom server port in META-INF/microprofile-config.properties then your server will listen on that port.

In your application code, Helidon uses the default configuration when you create a Server object without a custom Config object. See the following code from the project you created.

View Main.startServer:
    static Server startServer() {
        return Server.create().start(); 
    }
Copied
  • There is no Config object being used during server creation, so the default configuration is used.

Source precedence for default configuration

In order to properly configure your application using configuration sources, you need to understand the precedence rules that Helidon uses to merge your configuration data. By default, Helidon will use the following sources in precedence order:

  1. Java system properties
  2. Environment variables
  3. Properties specified in META-INF/microprofile-config.properties

Each of these sources specify configuration properties in Java Property format (key/value), like color=red. If any of the Helidon required properties are not specified in one of these source, like server.port, then Helidon will use a default value.

Because environment variable names are restricted to alphanumeric characters and underscore, Helidon adds aliases to the environment configuration source, allowing entries with dotted and/or hyphenated keys to be overriden. For example, this mapping allows an environment variable named "APP_GREETING" to override an entry key named "app.greeting". In the same way, an environment variable named "APP_dash_GREETING" will map to "app-greeting". See Advanced Config for more information.

The following examples will demonstrate the default precedence order.

Default configuration resource

Change a configuration parameter in the default configuration resource file, META-INF/microprofile-config.properties. There are no environment variable or system property overrides defined.

Change app.greeting in the META-INF/microprofile-config.properties from Hello to HelloFromMPConfig:
app.greeting=HelloFromMPConfig
Copied
Build the application, skipping unit tests, then run it:
mvn package -DskipTests=true
java -jar target/helidon-quickstart-mp.jar
Copied
Run the curl command in a new terminal window and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromMPConfig World!" 
}
Copied
  • The new app.greeting value in META-INF/microprofile-config.properties is used.
Environment variable override

An environment variable has a higher precedence than the configuration properties file.

Set the environment variable and restart the application:
export APP_GREETING=HelloFromEnvironment
java -jar target/helidon-quickstart-mp.jar
Copied
Invoke the endpoint below and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromEnvironment World!" 
}
Copied
  • The environment variable took precedence over the value in META-INF/microprofile-config.properties.
System property override

A system property has a higher precedence than environment variables.

Restart the application with a system property. The app.greeting environment variable is still set:
java -Dapp.greeting="HelloFromSystemProperty"  -jar target/helidon-quickstart-mp.jar
Copied
Invoke the endpoint below and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromSystemProperty World!" 
}
Copied
  • The system property took precedence over both the environment variable and META-INF/microprofile-config.properties.

Custom configuration sources

To use custom configuration sources, your application needs to use a Config object when creating a Server object. When you use a Config object, you are in full control of all configuration sources and precedence. By default, the environment variable and system property sources are enabled, but you can disable them using the disableEnvironmentVariablesSource and disableSystemPropertiesSource methods.

This section will show you how to use a custom configuration with various sources, formats, and precedence rules.

Full list of configuration sources

Here is the full list of external config sources that you can use programmatically.

  1. Java system properties - the property is a name/value pair.
  2. Environment variables - the property is a name/value pair.
  3. Resources in the classpath - the contents of the resource is parsed according to its inferred format.
  4. File - the contents of the file is parsed according to its inferred format.
  5. Directory - each non-directory file in the directory becomes a config entry: the file name is the key. and the contents of that file are used as the corresponding config String value.
  6. A URL resource - contents is parsed according to its inferred format.

You can also define custom sources, such as Git, and use them in your Helidon application. See Advanced Config for more information.

Classpath sources

The first custom resource example demonstrates how to add a second internal configuration resource that is discovered in the classpath. The code needs to build a Config object, which in turn is used to build the Server object. The Config object is built using a Config.Builder, which lets you inject any number of sources into the builder. Furthermore, you can set precedence for the sources. The first source has highest precedence, then the next has second highest, and so forth.

Add a resource file, named config.properties to the resources directory with the following contents:
app.greeting=HelloFrom-config.properties
Copied
Update the Main class; 1) Add new imports, 2) Replace the startServer method, and 3) Add buildConfig method:
import io.helidon.config.Config; 
import static io.helidon.config.ConfigSources.classpath;
...

  static Server startServer() {
        return Server.builder()
            .config(buildConfig()) 
            .build()
            .start();
    }

  private static Config buildConfig() {
    return Config.builder()
        .disableEnvironmentVariablesSource() 
        .sources(
            classpath("config.properties"), 
            classpath("META-INF/microprofile-config.properties")) 
        .build();
  }
Copied
  • Import config classes.
  • Pass the custom Config object to the Server.Builder.
  • Disable the environment variables as a source.
  • Specify the new config.properties resource that is in the classpath.
  • You must specify the existing META-INF/microprofile-config.properties or Helidon will not use it as a configuration source even though it is considered a default source.
Build and run the application (without the system property). Invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFrom-config.properties World!"
}
Copied
  • The greeting was picked up from config.properties, overriding the value in META-INF/microprofile-config.properties.

It is important to remember that configuration from all sources is merged internally. If you have the same configuration property in multiple sources, then only the one with highest precedence will be used at runtime. This is true even the same property comes from sources with different formats.

Swap the source order and run the test again.

Update the Main class and replace the buildConfig method:
  private static Config buildConfig() {
      return Config.builder()
          .disableEnvironmentVariablesSource()
          .sources(
              classpath("META-INF/microprofile-config.properties"), 
              classpath("config.properties"))
          .build();
  }
Copied
  • Swap the source order, putting META-INF/microprofile-config.properties first.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromMPConfig World!" 
}
Copied
  • The file META-INF/microprofile-config.properties was used to get the greeting since it now has precedence over config.properties.

External file sources

You can move all or part of your configuration to external files, making them optional or mandatory. The obvious advantage to this approach is that you do not need to rebuild your application to change configuration. In the following example, the app.greeting configuration property will be added to config-file.properties.

Unset the environment variable so that disableEnvironmentVariablesSource doesn’t need to be called:
unset APP_GREETING
Copied
Create a file named config-file.properties in the helidon-quickstart-mp directory with the following contents:
app.greeting=HelloFromConfigFile
Copied
Update the Main class; 1) Add new import and 2) Replace the buildConfig method:
import static io.helidon.config.ConfigSources.file;
...

  private static Config buildConfig() {
      return Config.builder()
          .sources(
              file("config-file.properties"), 
              classpath("META-INF/microprofile-config.properties"))
          .build();
  }
Copied
  • Add a mandatory configuration file.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromConfigFile World!"
}
Copied
  • The configuration property from the file config-file.properties takes precedence.

If you want the configuration file to be optional, you must use the optional method with sources, otherwise Helidon will generate an error during startup as shown below. This is true for both file and classpath sources. By default, these sources are mandatory.

Update the Main class and replace the buildConfig method:
  private static Config buildConfig() {
      return Config.builder()
          .sources(
              file("missing-file"), 
              classpath("META-INF/microprofile-config.properties"))
          .build();
  }
Copied
  • Specify a file that doesn’t exist.
Build then start the application and you will see the following output:
Exception in thread "main" io.helidon.config.ConfigException: Cannot load data from mandatory source FileConfig[missing-file]. File `missing-file` not found.
Copied

To fix this, use the optional method as shown below, then rerun the test.

...
    file("missing-file").optional(), 
  • The missing-file configuration file is now optional.

Directory source

A directory source treats every file in the directory as a key, and the file contents as the value. The following example includes a directory source as highest precedence.

Create a new directory helidon-quickstart-mp/conf then create a file named app.greeting in that directory with the following contents:
HelloFromFileInDirectoryConf
Copied
Update the Main class; 1) Add new import and 2) Replace the buildConfig method:
import static io.helidon.config.ConfigSources.directory;
...

  private static Config buildConfig() {
      return Config.builder()
          .sources(
              directory("conf"), 
              classpath("config.properties").optional(),
              classpath("META-INF/microprofile-config.properties"))
          .build();
  }
Copied
  • Add a mandatory configuration directory.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromFileInDirectoryConf World!"
}
Copied
  • The greeting was fetched from the file named app.greeting.

Exceeding three sources

If you have more than three sources, you need to use a ConfigSources class to create a custom source list as shown below.

Update the Main class; 1) Add new import and 2) Replace the buildConfig method:
import io.helidon.config.ConfigSources;
...

  private static Config buildConfig() {
      return Config.builder()
          .sources(ConfigSources.create(   
              directory("conf"),
              file("config-file.properties"),
              classpath("config.properties").optional(),
              classpath("META-INF/microprofile-config.properties")))
          .build();
  }
Copied
  • Create a list of four sources using ConfigSources.create method.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...

{
  "message": "HelloFromFileInDirectoryConf World!"
}
Copied

Meta-configuration

Instead of directly specifying the configuration sources in your code, you can use meta-configuration in a file that declares the configuration sources and their attributes. This requires using the Config.loadSourcesFrom method rather than a Config.Buider object. The contents of the meta-configuration file needs to be in JSON, YAML, or HOCON format. YAML is used in the following example.

Create a file named meta-config.yaml in the helidon-quickstart-mp directory with the following contents:
sources:
  - type: "classpath" 
    properties:
      resource: "META-INF/microprofile-config.properties" 
Copied
  • The source type.
  • The name of the mandatory configuration resource.
Update the Main class and replace the buildConfig method:
  private static Config buildConfig() {
      return Config.loadSourcesFrom( file("meta-config.yaml")); 
  }
Copied
  • Specify the meta-configuration file, which contains a single configuration source.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromMPConfig World!" 
}
Copied
  • The META-INF/microprofile-config.properties resource file was used to get the greeting.

The source precedence order in a meta-configuration file is the order of appearance in the file. This is demonstrated below where the config-file.properties has highest precedence.

Replace the contents of the meta-config.yaml file:
sources:
  - type: "file" 
    properties:
      path: "./config-file.properties" 
  - type: "classpath"
    properties:
      resource: "META-INF/microprofile-config.properties"
  - type: "file"
    properties:
      path: "optional-config-file"
      optional: true  
Copied
  • The source type specifies a file.
  • The name of the mandatory configuration file.
  • Specify that the optional-config-file file is optional.
Restart the application, then invoke the endpoint below and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromConfigFile World!" 
}
Copied
  • The config-file.properties source now takes precedence.

When using a meta-config file, you need to explicitly include both environment variables and system properties as a source if you want to use them.

Replace the contents of the meta-config.yaml file:
sources:
  - type: "environment-variables" 
  - type: "system-properties" 
  - type: "classpath"
    properties:
      resource: "META-INF/microprofile-config.properties"
  - type: "file"
    properties:
      path: "./config-file.properties"
Copied
  • Environment variables are now used as a source.
  • System properties are now used as a source.

You can re-run the previous tests that exercised environment variables and system properties. Swap the two types to see the precedence change. Be sure to unset APP_GREETING after you finish testing.

Accessing Config within an application

You have used Helidon to customize configuration behavior from your code using the Config and Config.Builder classes. The examples in this section will demonstrate how to access that config data at runtime. As discussed previously, Helidon reads configuration from a config source, which uses a config parser to translate the source into an immutable in-memory tree representing the configuration’s structure and values. Your application uses the Config object to access the in-memory tree, retrieving config data.

The generated project already accesses configuration data in the GreetingProvider class as follows:

View the following code from GreetingProvider.java:
@ApplicationScoped 
public class GreetingProvider {
    private final AtomicReference<String> message = new AtomicReference<>(); 

    @Inject
    public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) {   
        this.message.set(message);
    }

    String getMessage() {
        return message.get();
    }

    void setMessage(String message) {
        this.message.set(message);
    }
}
Copied
  • This class is application scoped so a single instance of GreetingProvider will be shared across the entire application.
  • Define a thread-safe reference that will refer to the message member variable.
  • The value of the configuration property app.greeting is injected into the GreetingProvider. constructor as a String parameter named message.
Injecting at field level

You can inject configuration at the field level as shown below. Use the volatile keyword since you cannot use AtomicReference with field level injection.

Update the meta-config.yaml with the following contents:
sources:
  - type: "classpath"
    properties:
      resource: "META-INF/microprofile-config.properties"  
Copied
  • This example only uses the default classpath source.
Update the following code from GreetingProvider.java:
@ApplicationScoped
public class GreetingProvider {

    @Inject
    @ConfigProperty(name = "app.greeting") 
    private volatile String message; 

    String getMessage() {
        return message;
    }

    void setMessage(String message) {
        this.message = message;
    }
}
Copied
  • Inject the value of app.greeting into the GreetingProvider object.
  • Define a class member variable to hold the greeting.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromMPConfig World!"
}
Copied
Injecting the Config object

You can inject the Config object into the class and access it directly as shown below. This object is not initialized when the GreetingProvider constructor is called, so you need to provide a method (onStartup) that observes @Initialized. This method will be called when GreetingProvider is ready for use.

Update the GreetingProvider.java file; 1) Add new imports and 2) Replace the GreetingProvider class:

import io.helidon.config.Config;
import javax.enterprise.context.Initialized;
import javax.enterprise.event.Observes;
...

@ApplicationScoped
public class GreetingProvider {
    private final AtomicReference<String> message = new AtomicReference<>();

    @Inject
    Config config;  

    public void onStartUp(@Observes @Initialized(ApplicationScoped.class) Object init) {
        message.set(config.get("app.greeting").asString().get()); 
    }

    String getMessage() {
        return message.get();
    }

    void setMessage(String message) {
        this.message.set(message);
    }
}
Copied
  • Add three new imports.
  • Inject the Config object into the GreetingProvider object.
  • Get the app.greeting value from the Config object and set the member variable.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromMPConfig World!"
}
Copied
Navigating the Config tree

Helidon offers a variety of methods to access in-memory configuration. These can be categorized as key access or tree navigation. You have been using key access for all of the examples to this point. For example app.greeting is accessing the greeting child node of the app parent node. There are many options for access this data using navigation methods as described in Hierarchical Config and Advanced Config. This simple example below demonstrates how to access a child node as a detached configuration sub-tree.

Create a file config-file.yaml in the helidon-quickstart-mp directory and add the following contents:
app:
  greeting:
    sender: Joe
    message: Hello-from-config-file.yaml
Copied
Update the meta-config.yaml with the following contents:
sources:
  - type: "classpath"
    properties:
      resource: "META-INF/microprofile-config.properties"
  - type: "file"
    properties:
      path: "./config-file.yaml"
Copied
Replace GreetingProvider class with the following code:
@ApplicationScoped
public class GreetingProvider {
    private final AtomicReference<String> message = new AtomicReference<>();
    private final AtomicReference<String> sender = new AtomicReference<>();

    @Inject
    Config config;

    public void onStartUp(@Observes @Initialized(ApplicationScoped.class) Object init) {
        Config appNode = config.get("app.greeting"); 
        message.set(appNode.get("message").asString().get());  
        sender.set(appNode.get("sender").asString().get());   
    }

    String getMessage() {
        return sender.get() + " says " + message.get();
    }

    void setMessage(String message) {
        this.message.set(message);
    }
}
Copied
  • Get the configuration subtree where the app.greeting node is the root.
  • Get the value from the message Config node.
  • Get the value from the sender Config node.
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "Joe says Hello-from-config-file.yaml World!"
}
Copied

Integration with Kubernetes

The following example uses a Kubernetes ConfigMap to pass the configuration data to your Helidon application deployed to Kubernetes. When the pod is created, Kubernetes will automatically create a local file within the container that has the contents of the configuration file used for the ConfigMap. This example will create the file at /etc/config/config-file.properties.

Update the Main class and replace the buildConfig method:
  private static Config buildConfig() {
      return Config.builder()
          .sources(
              file("/etc/config/config-file.properties").optional(), 
              classpath("META-INF/microprofile-config.properties")) 
          .build();
  }
Copied
  • The app.greeting value will be fetched from /etc/config/config-file.properties within the container.
  • The server port is specified in META-INF/microprofile-config.properties within the helidon-quickstart-mp.jar.
Update the following code from GreetingProvider.java:
@ApplicationScoped
public class GreetingProvider {

    @Inject
    @ConfigProperty(name = "app.greeting") 
    private volatile String message; 

    String getMessage() {
        return message;
    }

    void setMessage(String message) {
        this.message = message;
    }
}
Copied
Build and run the application, then invoke the endpoint and check the response:
curl http://localhost:8080/greet
...
{
  "message": "HelloFromConfigFile World!"
}
Copied
Stop the application and build the docker image:
docker build -t helidon-config-mp .
Copied
Generate a ConfigMap from config-file.properties:
kubectl create configmap helidon-configmap --from-file config-file.properties
Copied
View the contents of the ConfigMap:
kubectl get configmap helidon-configmap -o yaml
...
apiVersion: v1
data:
  config-file.properties: |   
    app.greeting=HelloFromConfigFile   
kind: ConfigMap
...
Copied
  • The file config-file.properties will be created within the Kubernetes container.
  • The config-file.properties file will have this single property defined.
Create the Kubernetes YAML specification, named k8s-config.yaml, with the following contents:
kind: Service
apiVersion: v1
metadata:
  name: helidon-config 
  labels:
    app: helidon-config
spec:
  type: NodePort
  selector:
    app: helidon-config
  ports:
    - port: 8080
      targetPort: 8080
      name: http
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: helidon-config
spec:
  replicas: 1 
  template:
    metadata:
      labels:
        app: helidon-config
        version: v1
    spec:
      containers:
        - name: helidon-config
          image: helidon-config-mp
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config 
      volumes:
        - name: config-volume
          configMap:
            # Provide the name of the ConfigMap containing the files you want
            # to add to the container
            name:  helidon-configmap 
Copied
  • A service of type NodePort that serves the default routes on port 8080.
  • A deployment with one replica of a pod.
  • Mount the ConfigMap as a volume at /etc/config. This is where Kubernetes will create config-file.properties.
  • Specify the ConfigMap which contains the configuration data.
Create and deploy the application into Kubernetes:
kubectl apply -f ./k8s-config.yaml
Copied
Get the service information:
kubectl get service/helidon-config
Copied
NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
helidon-config   NodePort   10.99.159.2   <none>        8080:31143/TCP   8s 
Copied
  • A service of type NodePort that serves the default routes on port 31143.
Verify the configuration endpoint using port 31143, your port will likely be different:
curl http://localhost:31143/greet
...
{
  "message": "HelloFromConfigFile World!" 
}
Copied
  • The greeting value from /etc/config/config-file.properties within the container was used.

You can now delete the Kubernetes resources that were just created during this example.

Delete the Kubernetes resources:
kubectl delete -f ./k8s-config.yaml
kubectl delete configmap  helidon-configmap
Copied

Summary

This guide has demonstrated how to use basic Helidon configuration features. The full configuration documentation, starting with the introduction section at Helidon Config has much more information including the following:

  • Architecture

  • Parsers

  • Extensions

  • Filters

  • Hierarchical Access

  • Property Mapping

  • Mutability Support

  • and more…​

Refer to the following references for additional information: