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.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();
}
}
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();
}
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();
}
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();
}
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();
}
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,theHostmapeia parathe-host. -
VERBATIM- O nome do método é usado como está para mapear a propriedade de configuração, ou seja,theHostmapeia paratheHost. -
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,theHostmapeia parathe_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();
}
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";
}
}
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();
}
}
}
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();
}
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();
}
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. |