Extending configuration support

1. Custom ConfigSource

It’s possible to create a custom ConfigSource as specified in MicroProfile Config.

With a Custom ConfigSource it is possible to read additional configuration values and add them to the Config instance in a defined ordinal. This allows overriding values from other sources or falling back to other values.

config sources

A custom ConfigSource requires an implementation of org.eclipse.microprofile.config.spi.ConfigSource or org.eclipse.microprofile.config.spi.ConfigSourceProvider. Each implementation requires registration via the ServiceLoader mechanism, either in META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource or META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider files.

1.1. 例子

Consider a simple in-memory ConfigSource:

org.acme.config.InMemoryConfigSource
package org.acme.config;

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

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class InMemoryConfigSource implements ConfigSource {
    private static final Map<String, String> configuration = new HashMap<>();

    static {
        configuration.put("my.prop", "1234");
    }

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

    @Override
    public Set<String> getPropertyNames() {
        return configuration.keySet();
    }

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

    @Override
    public String getName() {
        return InMemoryConfigSource.class.getSimpleName();
    }
}

And registration in:

META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
org.acme.config.InMemoryConfigSource

The InMemoryConfigSource will be ordered between the .env source, and the application.properties source due to the 275 ordinal:

ConfigSource Ordinal

System Properties

400

Environment Variables from System

300

Environment Variables from .env file

295

InMemoryConfigSource

275

application.properties from /config

260

application.properties from application

250

microprofile-config.properties from application

100

In this case, my.prop from InMemoryConfigSource will only be used if the config engine is unable to find a value in System Properties, Environment Variables from System or Environment Variables from .env file in this order.

1.2. ConfigSource Init

When a Quarkus application starts, a ConfigSource can be initialized twice. One time for STATIC INIT and a second time for RUNTIME INIT:

1.2.1. STATIC INIT

Quarkus starts some of its services during static initialization, and Config is usually one of the first things that is created. In certain situations it may not be possible to add a custom ConfigSource. For instance, if the ConfigSource requires other services, like a database access, it will not be available at this stage, and cause a chicken-egg problem. For this reason, any custom ConfigSource requires the annotation @io.quarkus.runtime.configuration.StaticInitSafe to mark the source as safe to be used at this stage.

1.2.1.1. 例子

Consider:

org.acme.config.InMemoryConfigSource
package org.acme.config;

import org.eclipse.microprofile.config.spi.ConfigSource;
import io.quarkus.runtime.annotations.StaticInitSafe;

@StaticInitSafe
public class InMemoryConfigSource implements ConfigSource {

}

And registration in:

META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
org.acme.config.InMemoryConfigSource

The InMemoryConfigSource will be available during STATIC INIT.

A custom ConfigSource is not automatically added during Quarkus STATIC INIT. It requires to be marked with the @io.quarkus.runtime.configuration.StaticInitSafe annotation.

1.2.2. RUNTIME INIT

The RUNTIME INIT stage happens after STATIC INIT. In this stage a ConfigSource can be initialized again. There are no restrictions at this stage, and a custom source is added to the Config instance as expected.

2. ConfigSourceFactory

Another way to create a ConfigSource is via the SmallRye Config io.smallrye.config.ConfigSourceFactory API. The difference between the SmallRye Config factory and the standard way to create a ConfigSource as specified in MicroProfile Config, is the factory ability to provide a context with access to the available configuration.

Each implementation of io.smallrye.config.ConfigSourceFactory requires registration via the ServiceLoader mechanism in the META-INF/services/io.smallrye.config.ConfigSourceFactory file.

2.1. 例子

Consider:

org.acme.config.URLConfigSourceFactory
package org.acme.config;

import java.util.Collections;
import java.util.OptionalInt;

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

import io.smallrye.config.ConfigSourceContext;
import io.smallrye.config.ConfigSourceFactory;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.PropertiesConfigSource;

public class URLConfigSourceFactory implements ConfigSourceFactory {
    @Override
    public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
        final ConfigValue value = context.getValue("config.url");
        if (value == null || value.getValue() == null) {
            return Collections.emptyList();
        }

        try {
            return Collections.singletonList(new PropertiesConfigSource(new URL(value.getValue())));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public OptionalInt getPriority() {
        return OptionalInt.of(290);
    }
}

And registration in:

META-INF/services/io.smallrye.config.ConfigSourceFactory
org.acme.config.URLConfigSourceFactory

By implementing io.smallrye.config.ConfigSourceFactory, a list of ConfigSource may be provided via the Iterable<ConfigSource> getConfigSources(ConfigSourceContext context) method. The ConfigSourceFactory may also assign a priority by overriding the method OptionalInt getPriority(). The priority values is used to sort multiple io.smallrye.config.ConfigSourceFactory (if found).

io.smallrye.config.ConfigSourceFactory priority does not affect the ConfigSource ordinal. These are sorted independently.

When the Factory is initializing, the provided ConfigSourceContext may call the method ConfigValue getValue(String name). This method looks up configuration names in all ConfigSources that were already initialized by the Config instance, including sources with lower ordinals than the ones defined in the ConfigSourceFactory. The ConfigSource list provided by a ConfigSourceFactory is not taken into consideration to configure other sources produced by a lower priority ConfigSourceFactory.

3. Custom Converter

It is possible to create a custom Converter type as specified by MicroProfile Config.

A custom Converter requires an implementation of org.eclipse.microprofile.config.spi.Converter<T>. Each implementation requires registration via the ServiceLoader mechanism in the META-INF/services/org.eclipse.microprofile.config.spi.Converter file. Consider:

package org.acme.config;

public class MicroProfileCustomValue {

    private final int number;

    public MicroProfileCustomValue(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
}

The corresponding converter will look similar to the one below.

package org.acme.config;

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

public class MicroProfileCustomValueConverter implements Converter<MicroProfileCustomValue> {

    @Override
    public MicroProfileCustomValue convert(String value) {
        return new MicroProfileCustomValue(Integer.parseInt(value));
    }
}
The custom converter class must be public, must have a public constructor with no arguments, and must not be abstract.

The custom configuration type converts the configuration value automatically:

@ConfigProperty(name = "configuration.value.name")
MicroProfileCustomValue value;

3.1. Converter priority

The jakarta.annotation.Priority annotation overrides the Converter priority and change converters precedence to fine tune the execution order. By default, if no @Priority is specified by the Converter, the converter is registered with a priority of 100. Consider:

package org.acme.config;

import jakarta.annotation.Priority;
import org.eclipse.microprofile.config.spi.Converter;

@Priority(150)
public class MyCustomConverter implements Converter<MicroProfileCustomValue> {

    @Override
    public MicroProfileCustomValue convert(String value) {

        final int secretNumber;
        if (value.startsFrom("OBF:")) {
            secretNumber = Integer.parseInt(SecretDecoder.decode(value));
        } else {
            secretNumber = Integer.parseInt(value);
        }

        return new MicroProfileCustomValue(secretNumber);
    }
}

Since it converts the same value type (MicroProfileCustomValue) and has a priority of 150, it will be used instead of a MicroProfileCustomValueConverter which has a default priority of 100.

All Quarkus core converters use the priority value of 200. To override any Quarkus specific converter, the priority value should be higher than 200.

4. Config Interceptors

SmallRye Config provides an interceptor chain that hooks into the configuration values resolution. This is useful to implement features like Profiles, Property Expressions, or just logging to find out where the config value was loaded from.

An interceptor requires an implementation of io.smallrye.config.ConfigSourceInterceptor. Each implementation requires registration via the ServiceLoader mechanism in the META-INF/services/io.smallrye.config.ConfigSourceInterceptor file.

The io.smallrye.config.ConfigSourceInterceptor exposes two interception points:

  • ConfigValue getValue(ConfigSourceInterceptorContext context, String name) — intercepts the resolution of a configuration value by name. The chain can be short-circuited by returning a custom instance of io.smallrye.config.ConfigValue.

  • Iterator<String> iterateNames(ConfigSourceInterceptorContext context) — intercepts the iteration of all known configuration property names. This affects the names returned by Config#getPropertyNames()`.

The ConfigSourceInterceptorContext is used to proceed with the interceptor chain. The ConfigValue objects hold information about the key name, value, config source origin and ordinal.

The interceptor chain is applied before any conversion is performed on the configuration value.

The ConfigSourceInterceptorContext provides two ways to continue the chain:

  • proceed(String name) — passes the lookup to the next interceptor in the chain. This is the standard method to use in most interceptors.

  • restart(String name) — re-invokes the first interceptor in the chain from the beginning. Intended for relocating or compatibility interceptors that resolve a new name and need the full chain to re-evaluate it, including profile expansion and expression resolution.

Passing the original name to restart can cause an infinite loop.

Interceptors may also be created with an implementation of io.smallrye.config.ConfigSourceInterceptorFactory. Each implementation requires registration via the ServiceLoader mechanism in the META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory file.

The ConfigSourceInterceptorFactory may initialize an interceptor with access to the current chain (so it can be used to configure the interceptor and retrieve configuration values) and set the priority.

4.1. 例子

org.acme.config.LoggingConfigSourceInterceptor
package org.acme.config;

import jakarta.annotation.Priority;

import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.Priorities;

@Priority(Priorities.APPLICATION)
public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor {
    @Override
    public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) {
        ConfigValue configValue = context.proceed(name);
        if (configValue != null) {
            System.out.println("Looked up: " + configValue.getName() + "=" + configValue.getValue()
                + " from " + configValue.getConfigSourceName());
        } else {
            System.out.println("Not found: " + name);
        }
        return configValue;
    }
}

And registration in:

META-INF/services/io.smallrye.config.ConfigSourceInterceptor
org.acme.config.LoggingConfigSourceInterceptor

The LoggingConfigSourceInterceptor logs each configuration name lookup. The log information includes the config name and value, the config source origin and location if they exist.

5. SecretKeysHandler

A SecretKeysHandler allows decoding or decrypting secret configuration values expressed as ${handler::value}. Custom handlers can be implemented via io.smallrye.config.SecretKeysHandler or io.smallrye.config.SecretKeysHandlerFactory. Each implementation requires registration via the ServiceLoader mechanism in META-INF/services/io.smallrye.config.SecretKeysHandler or META-INF/services/io.smallrye.config.SecretKeysHandlerFactory.

5.1. LazySecretKeysHandler

SecretKeysHandlerFactory is initialized during the first phase of SmallRye Config bootstrap, alongside regular ConfigSource and ConfigSourceProvider registrations. This means that configuration values produced by a ConfigSourceFactory are not yet available when the factory’s getSecretKeysHandler is called.

For handlers that depend on sources provided by a ConfigSourceFactory, wrap an inner SecretKeysHandlerFactory in SecretKeysHandlerFactory.LazySecretKeysHandler. The inner factory’s getSecretKeysHandler is only invoked the first time a value actually needs to be decoded, by which point all sources — including those from ConfigSourceFactory — are fully initialized:

package org.acme.config;

import io.smallrye.config.ConfigSourceContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.SecretKeysHandler;
import io.smallrye.config.SecretKeysHandlerFactory;

public class VaultSecretKeysHandlerFactory implements SecretKeysHandlerFactory {
    @Override
    public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) {
        return new LazySecretKeysHandler(new SecretKeysHandlerFactory() {
            @Override
            public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) {
                // This runs lazily, after all sources (including ConfigSourceFactory sources) are ready.
                ConfigValue token = context.getValue("vault.token");
                return new VaultSecretKeysHandler(token.getValue());
            }

            @Override
            public String getName() {
                return "vault";
            }
        });
    }

    @Override
    public String getName() {
        return "vault";
    }
}
Do not call context.getValue in the outer getSecretKeysHandler. Only the inner factory’s getSecretKeysHandler (invoked lazily) may resolve configuration values. Calling context.getValue in the outer factory will not have access to ConfigSourceFactory sources.