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

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 compartilhe o mesmo prefixo.

1. @ConfigMapping

Um mapeamento de configuração requer uma interface pública com o mínimo de configuração 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 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 consultada é construído a partir do prefixo e do nome do método, usando o . (ponto) como separador.

Se um mapeamento não encontrar uma correspondência para uma propriedade de configuração, uma NoSuchElementException é lançada, a menos que o elemento mapeado seja um Optional.

1.1. Registro

Quando uma aplicação Quarkus inicia, 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 (static initialization), e o Config é geralmente uma das primeiras coisas a ser criada. Em certas 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 nesta etapa. 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 existem restrições nesse estágio, e qualquer mapeamento de configuração é adicionado à instância de 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 que não são CDI, utilize 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 subagrupar 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 um nome de método ou um nome de propriedade não corresponderem um ao outro, a anotação @WithName pode sobrescrever o mapeamento do nome do método e utilizar 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 a propriedade de mapeamento de configuração herde o nome do seu contêiner, simplificando o nome da propriedade de configuração necessário para corresponder ao mapeamento:

@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

Sem a @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 suporta as seguintes estratégias de nomeação com os seguintes valores de enum:

  • 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, ou seja, theHost mapeia para the-host.

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

  • SNAKE_CASE - O nome do método é derivado pela substituição das letras maiúsculas por um sublinhado para mapear a propriedade de configuração, ou seja, theHost mapeia para the_host.

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 seus similares:

@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(), o valor retornado será 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

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, o valor de configuração é uma cadeia de caracteres separada por vírgulas.

Somente o mapeamento List pode manter a ordem dos elementos. Portanto, com os mapeamentos Set , a ordem dos elementos não é mantida a partir dos arquivos de configuração, mas é aleatória.

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 Map form() contém três elementos com as chaves login-page, error-page e landing-page.

Isso também funciona para grupos:

@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

Nesse caso, o Map allServers() conterá um elemento Server com a chave 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")
@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;
    }
}

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.

Related content