The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Mapeando configuração para objetos

Com os mapeamentos de configuração, é possível agrupar várias propriedades de configuração em uma única interface que compartilha o mesmo prefixo.

1. @ConfigMapping

Um mapeamento de configuração requer uma interface pública com configuração mínima de metadados e anotada com a anotação @io.smallrye.config.ConfigMapping.

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

    int port();
}

A interface Server é capaz de mapear as propriedades de configuração com o nome server.host para o método Server.host() e server.port para o método Server.port(). O nome da propriedade de configuração a ser pesquisada é criado a partir do prefixo e do nome do método com . (ponto) como separador.

Se um mapeamento não corresponder a uma propriedade de configuração, será lançado um NoSuchElementException, a menos que o elemento mapeado seja um Optional.

1.1. Registro

Quando um aplicativo Quarkus é iniciado, um mapeamento de configuração pode ser registrado duas vezes. Uma vez para STATIC INIT e uma segunda vez para RUNTIME INIT :

1.1.1. STATIC INIT

O Quarkus inicia alguns de seus serviços durante a inicialização estática, e Config geralmente é uma das primeiras coisas criadas. Em determinadas situações, pode não ser possível inicializar corretamente um mapeamento de configuração. Por exemplo, se o mapeamento exigir valores de um ConfigSource personalizado. Por esse motivo, qualquer mapeamento de configuração requer a anotação @io.quarkus.runtime.configuration.StaticInitSafe para marcar o mapeamento como seguro para ser usado nesse estágio. Saiba mais sobre o registro de um ConfigSource personalizado.

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

    int port();
}

1.1.2. RUNTIME INIT

O estágio RUNTIME INIT ocorre após o STATIC INIT. Não há restrições nesse estágio, e qualquer mapeamento de configuração é adicionado à instância Config conforme esperado.

1.2. Recuperação

Uma interface de mapeamento de configuração pode ser injetada em qualquer bean com reconhecimento de CDI:

class BusinessBean {
    @Inject
    Server server;

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

Em contextos não-CDI, use a API io.smallrye.config.SmallRyeConfig#getConfigMapping para recuperar a instância de mapeamento de configuração:

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

1.3. Grupos aninhados

Um mapeamento aninhado oferece uma maneira de subgrupar outras propriedades de configuração:

@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

O nome do método de um grupo de mapeamento atua como sub-namespace para as propriedades de configuração.

1.4. Sobrescrevendo nomes de propriedades

1.4.1. @WithName

Se o nome de um método ou o nome de uma propriedade não corresponderem um ao outro, a anotação @WithName poderá substituir o mapeamento do nome do método e usar o nome fornecido na anotação:

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

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

1.4.2. @WithParentName

A anotação @WithParentName permite que o mapeamento de configurações herde seu nome de contêiner, simplificando o nome da propriedade de configuração necessária para corresponder ao mapeamento:

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

Sem o @WithParentName, o método name() requer a propriedade de configuração server.info.name. Como usamos @WithParentName, o mapeamento info() herdará o nome pai de Server e name() mapeará para server.name.

1.4.3. NamingStrategy

Os nomes de métodos em camelCase são mapeados para nomes de propriedades em kebab-case:

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

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

A estratégia de mapeamento pode ser ajustada com a configuração do valor namingStrategy na anotação @ConfigMapping:

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

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

A anotação @ConfigMapping é compatível com as seguintes estratégias de nomeação:

  • KEBAB_CASE (padrão) - O nome do método é derivado substituindo as mudanças de maiúsculas por um traço para mapear a propriedade de configuração.

  • VERBATIM - O nome do método é usado como está para mapear a propriedade de configuração.

  • SNAKE_CASE - O nome do método é derivado pela substituição das mudanças de maiúsculas por um sublinhado para mapear a propriedade de configuração.

1.5. Conversões

Uma classe de mapeamento de configuração suporta conversões automáticas de todos os tipos disponíveis para conversão em 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

Isso também é válido para Optional e amigos:

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

    Optional<String> optional();

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

    interface Server {
        String host();

        int port();
    }
}

Nesse caso, o mapeamento não falhará se não houver nenhuma propriedade de configuração que corresponda ao mapeamento.

1.5.1. @WithConverter

A anotação @WithConverter oferece uma maneira de definir um Converter para ser usado em um mapeamento específico:

@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

Uma chamada para Converters.foo() resulta no valor bar.

1.5.2. Coleções

Um mapeamento de configuração também é capaz de mapear os tipos de coleções List e 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

Os mapeamentos List ou Set podem usar propriedades indexadas para mapear valores de configuração em grupos de mapeamento. Para coleções com tipos de elementos simples como String, seu valor de configuração é uma cadeia de caracteres separada por vírgulas.

1.5.3. Mapas

Um mapeamento de configuração também é capaz de mapear um 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

A propriedade de configuração precisa especificar um nome adicional para atuar como chave. Nesse caso, o form() Map conterá três elementos com as chaves login-page, error-page e 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

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

1.6. Padrões

A anotação @WithDefault permite definir uma propriedade padrão em um mapeamento (e evitar um erro se o valor da configuração não estiver disponível em nenhum ConfigSource):

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

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

Não são necessárias propriedades de configuração. O Defaults.foo() retornará o valor foo e o Defaults.bar() retornará o valor bar.

1.7. Validação

Um mapeamento de configuração pode combinar anotações do Bean Validation para validar os valores de configuração:

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

    @Max(10000)
    int port();
}
Para que a validação funcione, é necessária a extensão quarkus-hibernate-validator, que é executada automaticamente.

1.8. Mocking (Simulação)

Uma implementação de interface de mapeamento não é um proxy, portanto, não pode ser simulada diretamente com @InjectMock como outros beans CDI. Um truque é torná-la proxy com um método produtor:

public class ServerMockProducer {
    @Inject
    Config config;

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

O Server pode ser injetado como uma simulação em uma classe de teste do Quarkus com @InjectMock:

@QuarkusTest
class ServerMockTest {
    @InjectMock
    Server server;

    @Test
    void localhost() {
        Mockito.when(server.host()).thenReturn("localhost");
        assertEquals("localhost", server.host());
    }
}
O mock (simulação) é apenas uma casca vazia sem nenhum valor de configuração real.

Se o objetivo for apenas simular determinados valores de configuração e manter a configuração original, a instância de simulação exigirá um spy (espião):

@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;
    }
}

O AppConfig pode ser injetado como uma simulação em uma classe de teste do Quarkus com @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());
    }
}
Os elementos aninhados precisam ser espionados individualmente pelo Mockito.

Conteúdo Relacionado