Mapping configuration to objects
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.
If a mapping fails to match a configuration property a NoSuchElementException is thrown, unless the mapped
element is an 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.
Only the List mapping can maintain element order. Hence, with Set mappings the element order is not maintained from the configuration files but is random.
|
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();
}
For validation to work, the quarkus-hibernate-validator extension is required, and it is performed
automatically.
|
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());
}
}
| The mock is just an empty shell without any actual configuration values. |
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());
}
}
| Nested elements need to be spied individually by Mockito. |