Mapping configuration to objects

通过配置映射,可以将多个配置属性分组到一个共享相同前缀的单个接口中。

With config mappings it is possible to group multiple configuration properties in a single interface that share the same prefix.

@ConfigMapping

配置映射需要一个带有最小元数据配置的公共接口,并使用 @io.smallrye.config.ConfigMapping 注解进行注释。

A config mapping requires a public interface with minimal metadata configuration and annotated with the @io.smallrye.config.ConfigMapping annotation.

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();
}

Server 接口能够使用名称 server.host 将配置属性映射到 Server.host() 方法并映射到 server.port 方法。要查找的配置属性名称是由前缀和方法名称(以 . (点)作为分隔符)构建的。

The Server interface is able to map configuration properties with the name server.host into the Server.host() method and server.port into Server.port() method. The configuration property name to look up is built from the prefix, and the method name with . (dot) as the separator.

如果映射未匹配到配置属性,则会抛出 NoSuchElementException,除非映射的元素是 Optional

If a mapping fails to match a configuration property a NoSuchElementException is thrown, unless the mapped element is an Optional.

Registration

当 Quarkus 应用程序启动时,可以将配置映射注册两次。一次用于 STATIC INIT,第二次用于 RUNTIME INIT

When a Quarkus application starts, a config mapping can be registered twice. One time for STATIC INIT and a second time for RUNTIME INIT:

STATIC INIT

Quarkus 在静态初始化期间启动其一些服务,而 Config 通常是最先创建的事物之一。在特定情况下可能无法正确初始化配置映射。例如,如果映射需要来自自定义 ConfigSource 的值。出于此原因,任何配置映射都需要 @io.quarkus.runtime.configuration.StaticInitSafe 注解将映射标记为在此阶段安全使用。了解有关自定义 ConfigSourceregistration 的更多信息。

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 correctly initialize a config mapping. For instance, if the mapping requires values from a custom ConfigSource. For this reason, any config mapping requires the annotation @io.quarkus.runtime.configuration.StaticInitSafe to mark the mapping as safe to be used at this stage. Learn more about registration of a custom ConfigSource.

Example
@StaticInitSafe
@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();
}

RUNTIME INIT

RUNTIME INIT 阶段发生在 STATIC INIT 之后。此阶段没有限制,并且任何配置映射都会按预期添加到 Config 实例。

The RUNTIME INIT stage happens after STATIC INIT. There are no restrictions at this stage, and any config mapping is added to the Config instance as expected.

Retrieval

可以将配置映射接口注入到任何 CDI 感知 Bean 中:

A config mapping interface can be injected into any CDI aware bean:

class BusinessBean {
    @Inject
    Server server;

    public void businessMethod() {
        String host = server.host();
    }
}

在非 CDI 上下文中,使用 API io.smallrye.config.SmallRyeConfig#getConfigMapping 检索配置映射实例:

In non-CDI contexts, use the API io.smallrye.config.SmallRyeConfig#getConfigMapping to retrieve the config mapping instance:

SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class);
Server server = config.getConfigMapping(Server.class);

Nested groups

嵌套映射提供了一种对其他配置属性进行子分组的方法:

A nested mapping provides a way to subgroup other config properties:

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();

    Log log();

    interface Log {
        boolean enabled();

        String suffix();

        boolean rotate();
    }
}
application.properties
server.host=localhost
server.port=8080
server.log.enabled=true
server.log.suffix=.log
server.log.rotate=false

映射组的方法名称充当配置属性的子名称空间。

The method name of a mapping group acts as sub-namespace to the configurations properties.

Overriding property names

@WithName

如果方法名称或属性名称彼此不匹配,`@WithName`注释可以覆盖方法名称映射并使用注释中提供的名称:

If a method name, or a property name do not match with each other, the @WithName annotation can override the method name mapping and use the name supplied in the annotation:

@ConfigMapping(prefix = "server")
public interface Server {
    @WithName("name")
    String host();

    int port();
}
application.properties
server.name=localhost
server.port=8080

@WithParentName

`@WithParentName`注释允许配置映射继承其容器名称,简化了与映射匹配所需的配置属性名称:

The @WithParentName annotation allows to configurations mapping to inherit its container name, simplifying the configuration property name required to match the mapping:

interface Server {
    @WithParentName
    ServerHostAndPort hostAndPort();

    @WithParentName
    ServerInfo info();
}

interface ServerHostAndPort {
    String host();

    int port();
}

interface ServerInfo {
    String name();
}
application.properties
server.host=localhost
server.port=8080
server.name=konoha

如果没有 @WithParentName,方法 name()`需要配置属性 `server.info.name。因为我们使用了 @WithParentName,所以 info()`映射将从 `Server`中继承父名称,`name()`反而映射到了 `server.name

Without the @WithParentName the method name() requires the configuration property server.info.name. Because we use @WithParentName, the info() mapping will inherit the parent name from Server and name() maps to server.name instead.

NamingStrategy

使用骆驼式命名法的方法名称映射为短横线分隔式属性名称:

Method names in camelCase map to kebab-case property names:

@ConfigMapping(prefix = "server")
public interface Server {
    String theHost();

    int thePort();
}
application.properties
server.the-host=localhost
server.the-port=8080

可以通过在 `@ConfigMapping`注释中设置 `namingStrategy`值来调整映射策略:

The mapping strategy can be adjusted by setting namingStrategy value in the @ConfigMapping annotation:

@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM)
public interface ServerVerbatimNamingStrategy {
    String theHost();

    int thePort();
}
application.properties
server.theHost=localhost
server.thePort=8080

`@ConfigMapping`注释支持以下命名策略:

The @ConfigMapping annotation support the following naming strategies:

  • KEBAB_CASE (default) - The method name is derived by replacing case changes with a dash to map the configuration property.

  • VERBATIM - The method name is used as is to map the configuration property.

  • SNAKE_CASE - The method name is derived by replacing case changes with an underscore to map the configuration property.

Conversions

配置映射类支持自动转换 `Config`中可用转换的所有类型:

A config mapping class support automatic conversions of all types available for conversion in Config:

@ConfigMapping
public interface SomeTypes {
    @WithName("int")
    int intPrimitive();

    @WithName("int")
    Integer intWrapper();

    @WithName("long")
    long longPrimitive();

    @WithName("long")
    Long longWrapper();

    @WithName("float")
    float floatPrimitive();

    @WithName("float")
    Float floatWrapper();

    @WithName("double")
    double doublePrimitive();

    @WithName("double")
    Double doubleWrapper();

    @WithName("char")
    char charPrimitive();

    @WithName("char")
    Character charWrapper();

    @WithName("boolean")
    boolean booleanPrimitive();

    @WithName("boolean")
    Boolean booleanWrapper();
}
application.properties
int=9
long=9999999999
float=99.9
double=99.99
char=c
boolean=true

这也对 `Optional`及其支持的类型有效:

This is also valid for Optional and friends:

@ConfigMapping
public interface Optionals {
    Optional<Server> server();

    Optional<String> optional();

    @WithName("optional.int")
    OptionalInt optionalInt();

    interface Server {
        String host();

        int port();
    }
}

在这种情况下,如果没有配置属性与映射匹配,映射不会失败。

In this case, the mapping won’t fail if there is no configuration property to match the mapping.

@WithConverter

`@WithConverter`注释提供了一种在特定映射中使用 `Converter`的方法:

The @WithConverter annotation provides a way to set a Converter to use in a specific mapping:

@ConfigMapping
public interface Converters {
    @WithConverter(FooBarConverter.class)
    String foo();
}

public static class FooBarConverter implements Converter<String> {
    @Override
    public String convert(final String value) {
        return "bar";
    }
}
application.properties
foo=foo

Converters.foo()`的调用会生成值 `bar

A call to Converters.foo() results in the value bar.

Collections

配置映射还可以映射集合类型 List`和 `Set

A config mapping is also able to map collections types List and Set:

@ConfigMapping(prefix = "server")
public interface ServerCollections {
    Set<Environment> environments();

    interface Environment {
        String name();

        List<App> apps();

        interface App {
            String name();

            List<String> services();

            Optional<List<String>> databases();
        }
    }
}
application.properties
server.environments[0].name=dev
server.environments[0].apps[0].name=rest
server.environments[0].apps[0].services=bookstore,registration
server.environments[0].apps[0].databases=pg,h2
server.environments[0].apps[1].name=batch
server.environments[0].apps[1].services=stock,warehouse

List`或 `Set`映射可以使用 indexed properties在映射组中映射配置值。对于具有简单元素类型的集合(如 `String),其配置值是一个逗号分隔的字符串。

The List or Set mappings can use indexed properties to map configuration values in mapping groups. For collection with simple element types like String, their configuration value is a comma separated string.

Maps

配置映射还可以映射 Map

A config mapping is also able to map a Map:

@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();

    Map<String, String> form();
}
application.properties
server.host=localhost
server.port=8080
server.form.login-page=login.html
server.form.error-page=error.html
server.form.landing-page=index.html

配置属性需要指定要作为键的附加名称。在这种情况下,form() Map`将包含带有键 `login-page、`error-page`和 `landing-page`的三元素。

The configuration property needs to specify an additional name to act as the key. In this case the form() Map will contain three elements with the keys login-page, error-page and landing-page.

它也适用于组:

It also works for groups:

@ConfigMapping(prefix = "server")
public interface Servers {
    @WithParentName
    Map<String, Server> allServers();
}

public interface Server {
    String host();

    int port();

    String login();

    String error();

    String landing();
}
application.properties
server."my-server".host=localhost
server."my-server".port=8080
server."my-server".login=login.html
server."my-server".error=error.html
server."my-server".landing=index.html

在这种情况下,allServers() `Map`将包含一个带有键 `my-server`的 `Server`元素。

In this case the allServers() Map will contain one Server element with the key my-server.

Defaults

`@WithDefault`注释允许将默认属性设置为映射(如果配置值在任何 `ConfigSource`中不可用,则阻止和错误):

The @WithDefault annotation allows to set a default property into a mapping (and prevent and error if the configuration value is not available in any ConfigSource):

public interface Defaults {
    @WithDefault("foo")
    String foo();

    @WithDefault("bar")
    String bar();
}

不需要配置属性。Defaults.foo()`将返回值 `foo,而 Defaults.bar()`将返回值 `bar

No configuration properties required. The Defaults.foo() will return the value foo and Defaults.bar() will return the value bar.

Validation

配置映射可以组合来自 Bean Validation的注释来验证配置值:

A config mapping may combine annotations from Bean Validation to validate configuration values:

@ConfigMapping(prefix = "server")
public interface Server {
    @Size(min = 2, max = 20)
    String host();

    @Max(10000)
    int port();
}

为了使验证工作,`quarkus-hibernate-validator`扩展是必需的,并且会自动执行。

For validation to work, the quarkus-hibernate-validator extension is required, and it is performed automatically.

Mocking

映射接口实现不是代理,因此不能用 `@InjectMock`像其他 CDI bean一样直接模拟它。一种技巧是用生产者方法使它可代理:

A mapping interface implementation is not a proxy, so it cannot be mocked directly with @InjectMock like other CDI beans. One trick is to make it proxyable with a producer method:

public class ServerMockProducer {
    @Inject
    Config config;

    @Produces
    @ApplicationScoped
    @io.quarkus.test.Mock
    Server server() {
        return config.unwrap(SmallRyeConfig.class).getConfigMapping(Server.class);
    }
}

`Server`可以用 `@InjectMock`注入到 Quarkus 测试类中作为模拟:

The Server can be injected as a mock into a Quarkus test class with @InjectMock:

@QuarkusTest
class ServerMockTest {
    @InjectMock
    Server server;

    @Test
    void localhost() {
        Mockito.when(server.host()).thenReturn("localhost");
        assertEquals("localhost", server.host());
    }
}

模拟只是一个没有实际配置值的空壳。

The mock is just an empty shell without any actual configuration values.

如果目标只是模拟某些配置值并保留原始配置,则模拟实例需要一个间谍:

If the goal is to only mock certain configuration values and retain the original configuration, the mocking instance requires a spy:

@ConfigMapping(prefix = "app")
public interface AppConfig {
    @WithDefault("app")
    String name();

    Info info();

    interface Info {
        @WithDefault("alias")
        String alias();
        @WithDefault("10")
        Integer count();
    }
}

public static class AppConfigProducer {
    @Inject
    Config config;

    @Produces
    @ApplicationScoped
    @io.quarkus.test.Mock
    AppConfig appConfig() {
        AppConfig appConfig = config.unwrap(SmallRyeConfig.class).getConfigMapping(AppConfig.class);
        AppConfig appConfigSpy = Mockito.spy(appConfig);
        AppConfig.Info infoSpy = Mockito.spy(appConfig.info());
        Mockito.when(appConfigSpy.info()).thenReturn(infoSpy);
        return appConfigSpy;
    }
}

`AppConfig`可以用 `@Inject`注入到 Quarkus 测试类中作为模拟:

The AppConfig can be injected as a mock into a Quarkus test class with @Inject:

@QuarkusTest
class AppConfigTest {
    @Inject
    AppConfig appConfig;

    @Test
    void localhost() {
        Mockito.when(appConfig.name()).thenReturn("mocked-app");
        assertEquals("mocked-app", server.host());

        Mockito.when(appConfig.info().alias()).thenReturn("mocked-alias");
        assertEquals("mocked-alias", server.info().alias());
    }
}

嵌套元素需要由 Mockito 单独监视。

Nested elements need to be spied individually by Mockito.