Mapping configuration to objects

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

@ConfigMapping

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

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

    int port();
}

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

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

Registration

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

STATIC INIT

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

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

    int port();
}

RUNTIME INIT

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

Retrieval

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

class BusinessBean {
    @Inject
    Server server;

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

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

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

Nested groups

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

@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

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

Overriding property names

@WithName

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

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

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

@WithParentName

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

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

NamingStrategy

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

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

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

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

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

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

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

  • KEBAB_CASE(默认)- 方法名称通过使用破折号替换大小写变化而导出,以映射配置属性。

  • VERBATIM- 使用方法名称原样映射配置属性。

  • SNAKE_CASE- 方法名称通过使用下划线替换大小写变化而导出,以映射配置属性。

Conversions

配置映射类支持自动转换 `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`及其支持的类型有效:

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

    Optional<String> optional();

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

    interface Server {
        String host();

        int port();
    }
}

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

@WithConverter

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

@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

Collections

配置映射还可以映射集合类型 List`和 `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),其配置值是一个逗号分隔的字符串。

Maps

配置映射还可以映射 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`的三元素。

它也适用于组:

@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`元素。

Defaults

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

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

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

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

Validation

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

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

    @Max(10000)
    int port();
}

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

Mocking

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

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 测试类中作为模拟:

@QuarkusTest
class ServerMockTest {
    @InjectMock
    Server server;

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

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

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

@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 测试类中作为模拟:

@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 单独监视。