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.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();
}
}
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();
}
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();
}
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();
}
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
é 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();
}
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";
}
}
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();
}
}
}
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();
}
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();
}
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. |