编辑此页面

将配置映射到对象

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

1. @ConfigMapping

配置映射需要一个公共接口,其中包含最少的元数据配置,并用 @io.smallrye.config.ConfigMapping 注解进行标注。

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

    int port();
}

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

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

1.1. 注册

当 Quarkus 应用程序启动时,配置映射可以注册两次。一次用于静态初始化,一次用于运行时初始化

1.1.1. 静态初始化

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

1.1.1.1. 示例
@StaticInitSafe
@ConfigMapping(prefix = "server")
public interface Server {
    String host();

    int port();
}

1.1.2. 运行时初始化

运行时初始化阶段发生在静态初始化之后。在此阶段没有限制,任何配置映射都会按预期添加到 Config 实例中。

1.2. 检索

配置映射接口可以注入到任何支持 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);

1.3. 嵌套分组

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

@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

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

1.4. 覆盖属性名

1.4.1. @WithName

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

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

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

1.4.2. @WithParentName

@WithParentName 注解允许配置映射属性继承其容器名称,从而简化了匹配映射所需的配置属性名称。

@ConfigMapping(prefix = "server")
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

如果没有 @WithParentNamename() 方法需要配置属性 server.info.name。由于我们使用了 @WithParentNameinfo() 映射将继承 Server 的父名称,而 name() 将映射到 server.name

1.4.3. 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(默认)- 方法名通过将大小写变化替换为连字符来映射配置属性,即 theHost 映射到 the-host

  • VERBATIM - 方法名按原样使用以映射配置属性,即 theHost 映射到 theHost

  • SNAKE_CASE - 方法名通过将大小写变化替换为下划线来映射配置属性,即 theHost 映射到 the_host

1.5. 转换

配置映射类支持 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();
    }
}

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

1.5.1. @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 的值。

1.5.2. 集合

配置映射也能够映射集合类型 ListSet

@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

ListSet 映射可以使用索引属性来映射映射组中的配置值。对于具有简单元素类型(如 String)的集合,其配置值是以逗号分隔的字符串。

只有 List 映射可以保持元素顺序。因此,对于 Set 映射,元素顺序不从配置文件维护,而是随机的。

1.5.3. Map

配置映射也能够映射 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-pageerror-pagelanding-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 将包含一个 Server 元素,键为 my-server

1.6. 默认值

@WithDefault 注解允许为映射设置默认属性(并防止在任何 ConfigSource 中都找不到配置值时出错)。

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

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

不需要任何配置属性。Defaults.foo() 将返回 foo 的值,Defaults.bar() 将返回 bar 的值。

1.7. 验证

配置映射可以结合Bean Validation 的注解来验证配置值。

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

    @Max(10000)
    int port();
}
为了使验证生效,需要 quarkus-hibernate-validator 扩展,并且它会自动执行。

1.8. 模拟

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

public class ServerMockProducer {
    @Inject
    Config config;

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

Server 可以作为模拟对象注入到 Quarkus 测试类中,使用 @InjectMock

@QuarkusTest
class ServerMockTest {
    @InjectMock
    Server server;

    @Test
    void localhost() {
        Mockito.when(server.host()).thenReturn("localhost");
        assertEquals("localhost", server.host());
    }
}
模拟对象只是一个空壳,没有任何实际的配置值。

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

@ConfigMapping(prefix = "app")
@Unremovable
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 可以作为模拟对象注入到 Quarkus 测试类中,使用 @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 进行 spy。

相关内容