Escrevendo Sua Própria Extensão
As extensões Quarkus acrescentam um novo comportamento focado no programador na oferta principal e consistem em duas partes distintas, o aumento do tempo de construção e o contêiner de tempo de execução. A parte de aumento é responsável por todo o processamento de metadados, como a leitura de anotações, descritores XML, etc. O resultado desta fase de aumento é um bytecode registrado que é responsável por instanciar diretamente os serviços de tempo de execução relevantes.
Isso significa que os metadados são processados apenas uma vez no momento da construção, o que economiza no tempo de inicialização e também no uso da memória, pois as classes etc usadas para o processamento não são carregadas (ou mesmo presentes) na JVM em tempo de execução.
This is an in-depth documentation, see the building my first extension if you need an introduction, or the frequently asked questions. |
1. Filosofia da extensão
Esta seção é um trabalho em curso e reúne a filosofia segundo a qual as extensões devem ser concebidas e escritas.
1.1. Por que um framework de extensão
A missão do Quarkus é transformar toda a sua aplicação, inclusive as bibliotecas que ele usa, em um artefato que utiliza significativamente menos recursos do que as abordagens tradicionais. Esses recursos podem então ser usados para criar aplicações nativos usando GraalVM. Para fazer isso, você precisa analisar e entender o "mundo fechado" completo da aplicação. Sem o contexto total e completo, o melhor que se pode conseguir é um suporte genérico parcial e limitado. Ao usar a abordagem de extensão Quarkus, podemos alinhar as aplicações Java com ambientes com restrições de espaço de memória, como Kubernetes ou plataformas de nuvem.
O framework de extensão do Quarkus resulta em uma utilização de recursos significativamente melhor, mesmo quando o GraalVM não é usado (por exemplo, no HotSpot). Vamos listar as ações que uma extensão executa:
-
Recolher metadados de tempo de construção e gerar código
-
Esta parte não tem nada a ver com o GraalVM, é a forma como o Quarkus inicia os frameworks "no momento da construção"
-
O framework de extensão facilita a leitura de metadados, a pesquisa de classes e a geração de classes conforme necessário
-
Uma pequena parte do trabalho de extensão é executada em tempo de execução através das classes geradas, enquanto a maior parte do trabalho é feita em tempo de construção (chamado tempo de implantação)
-
-
Aplicar padrões sensatos e opinativos com base na visão global próxima da aplicação (por exemplo, uma aplicação sem
@Entity
não precisa iniciar o Hibernate ORM) -
Uma extensão hospeda a substituição de código da Substrate VM para que as bibliotecas possam ser executadas no GraalVM
-
A maioria das alterações são encaminhadas para a versão original do código para ajudar a biblioteca subjacente a rodar no GraalVM
-
Nem todas as alterações podem ser encaminhadas para a versão original do código, as extensões hospedam substituições da Substrate VM - que é uma forma de correção de código - para que as bibliotecas possam ser executadas
-
-
Hospedar a substituição de código da Substrate VM para ajudar a eliminar o código morto com base nas necessidades da aplicação
-
Isto depende da aplicação e não pode ser realmente compartilhado na própria biblioteca
-
Por exemplo, o Quarkus otimiza o código do Hibernate porque sabe que só precisa de um conjunto de conexões e de um fornecedor de cache específicos
-
-
Enviar metadados ao GraalVM para classes de exemplo que precisam de reflexão
-
Esta informação não é estática por biblioteca (por exemplo, Hibernate), mas o framework tem o conhecimento semântico e sabe quais as classes que precisam ter reflexão (por exemplo, classes @Entity)
-
1.2. Favorecer o trabalho em tempo de construção em detrimento do trabalho em tempo de execução
Na medida do possível, prefira fazer o trabalho no momento da construção(parte de implantação da extensão) em vez de deixar que o framework faça o trabalho no momento da inicialização (tempo de execução). Quanto mais trabalho for feito ali, menores serão as aplicações Quarkus que usam essa extensão e mais rápido elas serão carregadas.
1.3. Como expor a configuração
O Quarkus simplifica os usos mais comuns. Isso significa que seus padrões podem ser diferentes da biblioteca que ele integra.
Para tornar a experiência simples mais fácil, unifique a configuração em application.properties
por meio do SmallRye Config. Evite arquivos de configuração específicos de bibliotecas ou, pelo menos, torne-os opcionais: por exemplo, persistence.xml
para Hibernate ORM é opcional.
As extensões devem ver a configuração de forma holística como uma aplicação Quarkus em vez de se concentrarem na experiência da biblioteca. Por exemplo, quarkus.database.url
e amigos são compartilhados entre as extensões, pois a definição de um acesso ao banco de dados é uma tarefa compartilhada (em vez de uma propriedade hibernate.
, por exemplo). As opções de configuração mais úteis devem ser expostas como quarkus.[extension].
em vez do namespace natural da biblioteca. As propriedades menos comuns podem residir no namespace da biblioteca.
Para habilitar totalmente as suposições de mundo fechado que o Quarkus pode otimizar melhor, é melhor considerar as opções de configuração como estabelecidas no tempo de construção em vez de substituíveis no tempo de execução. É claro que propriedades como host, porta e senha devem ser substituíveis em tempo de execução. Mas muitas propriedades, como ativar o armazenamento em cache ou definir o driver JDBC, podem exigir com segurança uma reconstrução do aplicativo.
1.3.1. Configuração de Inicialização Estática
Se a extensão fornecer Fontes de Configuração adicionais e se estas forem necessárias durante a Inicialização Estática, devem ser registradas em StaticInitConfigBuilderBuildItem
. A configuração na Inicialização Estática não procura fontes adicionais para evitar a dupla inicialização no momento da inicialização da aplicação.
1.4. Exponha os seus componentes através de CDI
Como a CDI é o modelo de programação central no que se refere à composição de componentes, os frameworks e as extensões devem expor seus componentes como beans facilmente consumíveis pelas aplicações do usuário. Por exemplo, o Hibernate ORM expõe os beans EntityManagerFactory
e EntityManager
, o pool de conexões expõe os beans DataSource
etc. As extensões devem registrar essas definições de beans no momento da construção.
1.4.1. Beans apoiados por classes
Uma extensão pode produzir um AdditionalBeanBuildItem
para instruir o contêiner a ler uma definição de bean de uma classe como se esta fizesse parte da aplicação original:
AdditionalBeanBuildItem
@Singleton (1)
public class Echo {
public String echo(String val) {
return val;
}
}
1 | Se um bean registrado por um AdditionalBeanBuildItem não especificar um escopo, assume-se @Dependent . |
Todos os outros beans podem injetar esse bean:
AdditionalBeanBuildItem
@Path("/hello")
public class ExampleResource {
@Inject
Echo echo;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(String foo) {
return echo.echo(foo);
}
}
E vice-versa - o bean de extensão pode injetar beans de aplicação e beans fornecidos por outras extensões:
@Singleton
public class Echo {
@Inject
DataSource dataSource; (1)
@Inject
Instance<List<String>> listsOfStrings; (2)
//...
}
1 | Injetar um bean fornecido por outra extensão. |
2 | Injetar todos os beans que correspondam ao tipo List<String> . |
1.4.2. Inicialização do bean
Some components may require additional initialization based on information collected during augmentation. The most straightforward solution is to obtain a bean instance and call a method directly from a build step. However, it is illegal to obtain a bean instance during the augmentation phase. The reason is that the CDI container is not started yet. It’s started during the Static init bootstrap phase.
As raízes de configuração BUILD_AND_RUN_TIME_FIXED e RUN_TIME podem ser injetadas em qualquer bean. No entanto, raízes de configuração RUN_TIME só devem ser injetadas após o bootstrap.
|
It is possible to invoke a bean method from a recorder method though.
If you need to access a bean in a @Record(STATIC_INIT)
build step then is must either depend on the BeanContainerBuildItem
or wrap the logic in a BeanContainerListenerBuildItem
.
The reason is simple - we need to make sure the CDI container is fully initialized and started.
However, it is safe to expect that the CDI container is fully initialized and running in a @Record(RUNTIME_INIT)
build step.
You can obtain a reference to the container via CDI.current()
or Quarkus-specific Arc.container()
.
Não se esqueça de se certificar de que o estado do bean garante a visibilidade, por exemplo, através da palavra-chave volatile .
|
There is one significant drawback of this "late initialization" approach. An uninitialized bean may be accessed by other extensions or application components that are instantiated during bootstrap. We’ll cover a more robust solution in the Beans sintéticos. |
1.4.3. Beans padrões
Um padrão muito útil para criar esses beans, mas que também dá ao código do aplicativo a capacidade de substituir facilmente alguns beans com implementações personalizadas, é usar o @DefaultBean
que o Quarkus fornece. É melhor explicar isso com um exemplo.
Suponhamos que a extensão Quarkus precisa fornecer um Tracer
bean que o código da aplicação deve injetar nos seus próprios beans.
@Dependent
public class TracerConfiguration {
@Produces
public Tracer tracer(Reporter reporter, Configuration configuration) {
return new Tracer(reporter, configuration);
}
@Produces
@DefaultBean
public Configuration configuration() {
// create a Configuration
}
@Produces
@DefaultBean
public Reporter reporter(){
// create a Reporter
}
}
Se, por exemplo, o código da aplicação quiser utilizar Tracer
, mas também precisar utilizar um bean Reporter
personalizado, esse requisito pode ser facilmente cumprido utilizando algo do gênero:
@Dependent
public class CustomTracerConfiguration {
@Produces
public Reporter reporter(){
// create a custom Reporter
}
}
1.4.4. Como Substituir um Bean Definido por uma Biblioteca/Extensão Quarkus que não utiliza @DefaultBean
Embora @DefaultBean
seja a abordagem recomendada, também é possível que o código da aplicação substitua os beans fornecidos por uma extensão marcando-os como um CDI @Alternative
e incluindo a anotação @Priority
. Vamos mostrar um exemplo simples. Suponha que trabalhamos em uma extensão imaginária "quarkus-parser" e que temos uma implementação de bean padrão:
@Dependent
class Parser {
String[] parse(String expression) {
return expression.split("::");
}
}
E a nossa extensão também consome este parser:
@ApplicationScoped
class ParserService {
@Inject
Parser parser;
//...
}
Agora, se um usuário ou mesmo outra extensão precisa substituir a implementação padrão do Parser
, a solução mais simples é utilizar CDI @Alternative
+ @Priority
:
@Alternative (1)
@Priority(1) (2)
@Singleton
class MyParser extends Parser {
String[] parse(String expression) {
// my super impl...
}
}
1 | MyParser é um bean alternativo. |
2 | Habilita a alternativa. A prioridade pode ser qualquer número para substituir o bean padrão, mas se existirem várias alternativas, a prioridade mais elevada ganha. |
As alternativas CDI só são consideradas durante a injeção e a resolução da tipagem segura. Por exemplo, a implementação padrão continuaria a receber notificações do observador. |
1.4.5. Beans sintéticos
Às vezes, é muito útil poder registrar um bean sintético. Os atributos de bean de um bean sintético não são derivados de uma classe, método ou campo Java. Em vez disso, os atributos são especificados por uma extensão.
Como o contêiner CDI não controla a instanciação de um bean sintético, não há suporte para injeção de dependência e outros serviços (como interceptadores). Em outras palavras, cabe à extensão fornecer todos os serviços necessários a uma instância de bean sintético. |
There are several ways to register a synthetic bean in Quarkus. In this chapter, we will cover a use case that can be used to initialize extension beans in a safe manner (compared to Inicialização do bean).
O SyntheticBeanBuildItem
pode ser utilizado para registrar um bean sintético:
-
whose instance can be easily produced through a recorder,
-
para fornecer um bean de "contexto" que contém todas as informações recolhidas durante a ampliação, de modo a que os componentes reais não necessitem de qualquer "inicialização tardia" porque podem injetar diretamente o bean de contexto.
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.runtimeValue(recorder.createFoo("parameters are recorder in the bytecode")) (1)
.done();
}
1 | O valor da string é gravado no bytecode e utilizado para inicializar a instância de Foo . |
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(TestContext.class).scope(Singleton.class)
.runtimeValue(recorder.createContext("parameters are recorder in the bytecode")) (1)
.done();
}
1 | Os componentes "reais" podem injetar diretamente o TestContext . |
1.5. Alguns tipos de extensões
Existem vários estereótipos de extensão, vamos enumerar alguns.
- Biblioteca nua em funcionamento
-
Essa é a extensão menos sofisticada. Ela consiste em um conjunto de patches para garantir que uma biblioteca seja executada no GraalVM. Se possível, contribua com esses patches na fonte original, não em extensões. A segunda melhor opção é escrever substituições de Substrate VM, que são patches aplicados durante a compilação da imagem nativa.
- Colocar um framework em funcionamento
-
Um framework em tempo de execução normalmente lê a configuração, examina o classpath e as classes em busca de metadados (anotações, getters etc.), constrói um metamodelo sobre o qual é executado, encontra opções por meio do padrão do carregador de serviços, prepara chamadas de invocação (reflexão), interfaces de proxy etc. Essas operações devem ser feitas no momento da construção e o metamodelo deve ser passado para a DSL do gravador, que gerará classes que serão executadas no tempo de execução e inicializarão o framework.
- Colocar uma extensão portátil CDI em funcionamento
-
O modelo de extensão portátil do CDI é muito flexível. Flexível demais para se beneficiar da inicialização do tempo de construção promovida pelo Quarkus. A maioria das extensões que vimos não faz uso desses recursos de extrema flexibilidade. A maneira de portar uma extensão CDI para o Quarkus é reescrevê-la como uma extensão Quarkus que definirá os vários beans no momento da construção (tempo de implantação no jargão da extensão).
2. Aspecto técnico
2.1. Três Fases do Bootstrap e Filosofia do Quarkus
Há três fases distintas de bootstrap de uma aplicação Quarkus:
- Ampliação
-
This is the first phase, and is done by the Processadores de Etapas de Construção. These processors have access to Jandex annotation information and can parse any descriptors and read annotations, but should not attempt to load any application classes. The output of these build steps is some recorded bytecode, using an extension of the ObjectWeb ASM project called Gizmo(ext/gizmo), that is used to actually bootstrap the application at runtime. Depending on the
io.quarkus.deployment.annotations.ExecutionTime
value of the@io.quarkus.deployment.annotations.Record
annotation associated with the build step, the step may be run in a different JVM based on the following two modes. - Inicialização Estática
-
Se o bytecode for gravado com
@Record(STATIC_INIT)
, ele será executado a partir de um método de inicialização estático na classe principal. Para uma construção executável nativa, esse código é executado em uma JVM normal como parte do processo de construção nativa, e quaisquer objetos retidos que forem produzidos nesse estágio serão serializados diretamente no executável nativo por meio de um arquivo mapeado de imagem. Isso significa que, se um framework puder inicializar nessa fase, ela terá seu estado inicializado gravado diretamente na imagem e, portanto, o código de inicialização não precisará ser executado quando a imagem for iniciada.Existem algumas restrições sobre o que pode ser feito nesta fase, uma vez que a Substrate VM não permite alguns objetos no executável nativo. Por exemplo, não se deve tentar escutar numa porta ou iniciar threads nesta fase. Além disso, não é permitido ler a configuração em tempo de execução durante a inicialização estática.
No modo JVM puro não nativo, não há diferença real entre Inicialização Estática e Inicialização Em Tempo de Execução, exceto pelo fato de que a inicialização estática é sempre executada primeiro. Esse modo se beneficia da mesma ampliação da fase de construção que o modo nativo, pois a análise do descritor e a varredura de anotações são feitas no momento da construção e quaisquer dependências de classe/framework associadas podem ser removidas do jar de saída da construção. Em servidores como o WildFly, as classes relacionadas à implantação, como os analisadores XML, permanecem durante todo o tempo de vida da aplicação, utilizando uma memória valiosa. O objetivo do Quarkus é eliminar isso, de modo que as únicas classes carregadas em tempo de execução sejam realmente usadas em tempo de execução.
Por exemplo, o único motivo pelo qual uma aplicação Quarkus deve carregar um analisador XML é se o usuário estiver usando XML em sua aplicação. Qualquer análise de XML da configuração deve ser feita na fase de ampliação.
- Inicialização em Tempo de Execução
-
Se o bytecode for gravado com
@Record(RUNTIME_INIT)
, ele será executado a partir do método principal do aplicativo. Esse código será executado na inicialização do executável nativo. Em geral, o mínimo possível de código deve ser executado nessa fase e deve ser restrito ao código que precisa abrir portas etc.
Empurrar o máximo possível para a fase @Record(STATIC_INIT)
permite duas otimizações diferentes:
-
Tanto no modo executável nativo quanto no modo JVM puro, isso permite que a aplicação seja iniciada o mais rápido possível, pois o processamento foi feito durante o tempo de construção. Isto também minimiza as classes/código nativo necessário na aplicação para comportamentos puramente relacionados com o tempo de execução.
-
Outra vantagem do modo executável nativo é que o Substrate pode eliminar mais facilmente os recursos que não são usados. Se os recursos forem inicializados diretamente via bytecode, o Substrate pode detectar que um método nunca é chamado e eliminar esse método. Se a configuração for lida em tempo de execução, o Substrate não pode raciocinar sobre o conteúdo da configuração e, portanto, precisa manter todos os recursos, caso sejam necessários.
2.2. Configuração do projeto
O seu projeto de extensão deve ser configurado como um projeto multi-módulo com dois submódulos:
-
Um submódulo de tempo de implantação que trata do processamento do tempo de construção e da gravação de bytecode.
-
Um submódulo de tempo de execução que contém o comportamento de tempo de execução que fornecerá o comportamento de extensão no executável nativo ou na JVM de tempo de execução.
Your runtime artifact should depend on io.quarkus:quarkus-core
, and possibly the runtime artifacts of other Quarkus
modules if you want to use functionality provided by them.
Your deployment time module should depend on io.quarkus:quarkus-core-deployment
, your runtime artifact,
and the deployment artifacts of any other Quarkus extensions your own extension depends on. This is essential, otherwise any transitively
pulled in extensions will not provide their full functionality.
The Maven and Gradle plugins will validate this for you and alert you to any deployment artifacts you might have forgotten to add. |
Em nenhuma circunstância o módulo de tempo de execução pode depender de um artefato de implantação. Isso resultaria em puxar todo o código de tempo de implantação para o escopo de tempo de execução, o que anula o objetivo de ter a divisão. |
2.2.1. Utilizando o Maven
Você precisa incluir o io.quarkus:quarkus-extension-maven-plugin
e configurar o maven-compiler-plugin
para detectar o processador de anotações quarkus-extension-processor
para recolher e gerar os metadados de extensão Quarkus necessários para os artefatos de extensão. Se estiver utilizando o pom principal do Quarkus, este herdará automaticamente a configuração correta.
Você pode querer utilizar o mojo create-extension de io.quarkus.platform:quarkus-maven-plugin para criar estes módulos Maven - ver a próxima seção.
|
Por convenção, o artefato de tempo de implantação tem o sufixo -deployment e o artefato de tempo de execução não tem sufixo (e é o que o usuário final adiciona ao seu projeto).
|
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<!-- Executions configuration can be inherited from quarkus-build-parent -->
<executions>
<execution>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
A configuração maven-compiler-plugin acima requer a versão 3.5+.
|
Também será necessário configurar o maven-compiler-plugin
do módulo de implantação para detectar o processador de anotações quarkus-extension-processor
.
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.2.1.1. Criar novos módulos de extensão do Quarkus Core utilizando o Maven
O Quarkus fornece o Mojo create-extension
do Maven para inicializar o seu projeto de extensão.
Ele tentará detectar automaticamente as suas opções:
-
a partir do diretório
quarkus
(Quarkus Core) ouquarkus/extensions
, ele utilizará a layout de extensão 'Quarkus Core' e os padrões. -
com
-DgroupId=io.quarkiverse.[extensionId]
, ele utilizará o esquema e as predefinições da extensão 'Quarkiverse'. -
em outros casos, utilizará o layout e os padrões da extensão "Standalone".
-
podemos introduzir outros tipos de layout no futuro.
Você não pode especificar qualquer parâmetro para utilizar o modo interativo: mvn io.quarkus.platform:quarkus-maven-plugin:3.16.3:create-extension -N
|
Como exemplo, vamos adicionar uma nova extensão chamada my-ext
à árvore de origem do Quarkus:
git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn io.quarkus.platform:quarkus-maven-plugin:3.16.3:create-extension -N \
-DextensionId=my-ext \
-DextensionName="My Extension" \
-DextensionDescription="Do something useful."
Por padrão, groupId , version , quarkusVersion , namespaceId , e namespaceName serão consistentes com outras extensões do núcleo do Quarkus.
|
A descrição da extensão é importante, uma vez que é apresentada em https://code.quarkus.io/, ao listar extensões com o Quarkus CLI, etc. |
A sequência de comandos acima faz o seguinte:
-
Cria quatro novos módulos Maven:
-
quarkus-my-ext-parent
no diretórioextensions/my-ext
-
quarkus-my-ext
no diretórioextensions/my-ext/runtime
-
quarkus-my-ext-deployment
no diretórioextensions/my-ext/deployment
; neste módulo é gerada uma classe básicaMyExtProcessor
. -
quarkus-my-ext-integration-test
no diretóriointegration-tests/my-ext/deployment
; uma classe de recurso Jakarta REST vazia e duas classes de teste (para o modo JVM e o modo nativo) são geradas neste módulo.
-
-
Liga estes três módulos onde necessário:
-
quarkus-my-ext-parent
é adicionado ao<modules>
doquarkus-extensions-parent
-
quarkus-my-ext
é adicionado ao<dependencyManagement>
do BOM (Bill of Materials) do Quarkusbom/application/pom.xml
-
quarkus-my-ext-deployment
é adicionado ao<dependencyManagement>
do BOM (Bill of Materials) do Quarkusbom/application/pom.xml
-
quarkus-my-ext-integration-test
é adicionado ao sítio<modules>
dequarkus-integration-tests-parent
-
Você também precisa preencher o arquivo de modelo quarkus-extension.yaml que descreve a sua extensão dentro da pasta src/main/resources/META-INF do módulo de tempo de execução.
|
Este é o modelo quarkus-extension.yaml
da extensão quarkus-agroal
. Pode utilizá-lo como exemplo:
name: "Agroal - Database connection pool" (1)
metadata:
keywords: (2)
- "agroal"
- "database-connection-pool"
- "datasource"
- "jdbc"
guide: "https://quarkus.io/guides/datasource" (3)
categories: (4)
- "data"
status: "stable" (5)
1 | o nome da extensão que será apresentada aos usuários |
2 | palavras-chave que podem ser usadas para encontrar a extensão no catálogo de extensões |
3 | link para o guia ou documentação da extensão |
4 | as categorias em que a extensão deve aparecer em code.quarkus.io, podem ser omitidas, caso em que a extensão continuará a ser listada mas não numa categoria específica |
5 | estado de maturidade, que pode ser stable , preview ou experimental , avaliado pelos responsáveis pela extensão |
O parâmetro name do mojo é opcional. Se você não o especificar na linha de comando, o plug-in o derivará de extensionId substituindo os traços por espaços e colocando cada token em caixa alta. Portanto, você pode considerar omitir explicitamente name em alguns casos.
|
Consulte o JavaDoc do CreateExtensionMojo para conhecer todas as opções disponíveis do mojo.
2.2.2. Usando Gradle
Você precisará aplicar o plug-in io.quarkus.extension
no módulo runtime
do seu projeto de extensão. O plug-in inclui a tarefa extensionDescriptor
que gerará os arquivos META-INF/quarkus-extension.properties
e META-INF/quarkus-extension.yml
. O plug-in também habilita o processador de anotações io.quarkus:quarkus-extension-processor
nos módulos deployment
e runtime
para coletar e gerar o restante dos metadados da extensão Quarkus. O nome do módulo de implantação pode ser configurado no plug-in, definindo a propriedade deploymentModule
. A propriedade é definida como deployment
por padrão:
plugins {
id 'java'
id 'io.quarkus.extension'
}
quarkusExtension {
deploymentModule = 'deployment'
}
dependencies {
implementation platform('io.quarkus:quarkus-bom:3.16.3')
}
2.3. Processadores de Etapas de Construção
O trabalho é feito no momento da ampliação por etapas de construção que produzem e consomem itens de construção. As etapas de construção encontradas nos módulos de implantação que correspondem às extensões na construção do projeto são automaticamente conectadas e executadas para produzir o(s) artefato(s) de construção final.
2.3.1. Etapas de Construção
A build step is a non-static method which is annotated with the @io.quarkus.deployment.annotations.BuildStep
annotation.
Each build step may consume items that are produced by earlier stages, and produce items that can be consumed by later stages. Build steps are normally only run when they produce a build item that is
ultimately consumed by another step.
Build steps are normally placed on plain classes within an extension’s deployment module. The classes are automatically instantiated during the augment process and utilize injection.
2.3.2. Itens de Construção
Build items are concrete, final subclasses of the abstract io.quarkus.builder.item.BuildItem
class. Each build item represents
some unit of information that must be passed from one stage to another. The base BuildItem
class may not itself be directly
subclassed; rather, there are abstract subclasses for each of the kinds of build item subclasses that may be created:
simple, multi, and empty.
Pense nos itens de construção como uma forma das diferentes extensões comunicarem umas com as outras. Por exemplo, um item de construção pode:
-
expor o fato de existir uma configuração da base de dados
-
consumir essa configuração de base de dados (por exemplo, uma extensão de pool de conexões ou uma extensão ORM)
-
solicitar que uma extensão faça o trabalho para outra extensão: por exemplo, uma extensão que deseja definir um novo bean CDI e solicita que a extensão ArC o faça
Trata-se de um mecanismo muito flexível.
BuildItem devem ser imutáveis, pois o modelo produtor/consumidor não permite que a mutação seja ordenada corretamente. Isso não é imposto, mas a não observância dessa regra pode resultar em condições de corrida.
|
Os passos de construção são executados se e somente se eles produzem itens de construção que são (transitivamente) necessários para outros passos de construção. Certifique-se de que sua etapa de construção produz um item de construção, caso contrário, você provavelmente deve produzir ValidationErrorBuildItem para validações de construção, ou ArtifactResultBuildItem para artefatos gerados.
|
2.3.2.1. Itens de construção simples
Os itens de construção simples são classes finais que estendem io.quarkus.builder.item.SimpleBuildItem
. Os itens de construção simples só podem ser produzidos por uma etapa em uma determinada construção; se várias etapas em uma construção declararem que produzem o mesmo item de construção simples, será gerado um erro. Qualquer número de etapas de construção pode consumir um item de construção simples. Uma etapa de construção que consome um item de construção simples sempre será executada após a etapa de construção que produziu esse item.
/**
* The build item which represents the Jandex index of the application,
* and would normally be used by many build steps to find usages
* of annotations.
*/
public final class ApplicationIndexBuildItem extends SimpleBuildItem {
private final Index index;
public ApplicationIndexBuildItem(Index index) {
this.index = index;
}
public Index getIndex() {
return index;
}
}
2.3.2.2. Itens de construção múltipla
Itens de construção múltipla ou "multi" são classes finais que estendem io.quarkus.builder.item.MultiBuildItem
. Qualquer número de itens de construção múltipla de uma determinada classe pode ser produzido por qualquer número de etapas, mas qualquer etapa que consuma itens de construção múltipla só será executada após a execução de todas as etapas que podem produzi-los.
public final class ServiceWriterBuildItem extends MultiBuildItem {
private final String serviceName;
private final List<String> implementations;
public ServiceWriterBuildItem(String serviceName, String... implementations) {
this.serviceName = serviceName;
// Make sure it's immutable
this.implementations = Collections.unmodifiableList(
Arrays.asList(
implementations.clone()
)
);
}
public String getServiceName() {
return serviceName;
}
public List<String> getImplementations() {
return implementations;
}
}
/**
* This build step produces a single multi build item that declares two
* providers of one configuration-related service.
*/
@BuildStep
public ServiceWriterBuildItem registerOneService() {
return new ServiceWriterBuildItem(
Converter.class.getName(),
MyFirstConfigConverterImpl.class.getName(),
MySecondConfigConverterImpl.class.getName()
);
}
/**
* This build step produces several multi build items that declare multiple
* providers of multiple configuration-related services.
*/
@BuildStep
public void registerSeveralServices(
BuildProducer<ServiceWriterBuildItem> providerProducer
) {
providerProducer.produce(new ServiceWriterBuildItem(
Converter.class.getName(),
MyThirdConfigConverterImpl.class.getName(),
MyFourthConfigConverterImpl.class.getName()
));
providerProducer.produce(new ServiceWriterBuildItem(
ConfigSource.class.getName(),
MyConfigSourceImpl.class.getName()
));
}
/**
* This build step aggregates all the produced service providers
* and outputs them as resources.
*/
@BuildStep
public void produceServiceFiles(
List<ServiceWriterBuildItem> items,
BuildProducer<GeneratedResourceBuildItem> resourceProducer
) throws IOException {
// Aggregate all the providers
Map<String, Set<String>> map = new HashMap<>();
for (ServiceWriterBuildItem item : items) {
String serviceName = item.getName();
for (String implName : item.getImplementations()) {
map.computeIfAbsent(
serviceName,
(k, v) -> new LinkedHashSet<>()
).add(implName);
}
}
// Now produce the resource(s) for the SPI files
for (Map.Entry<String, Set<String>> entry : map.entrySet()) {
String serviceName = entry.getKey();
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (OutputStreamWriter w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
for (String implName : entry.getValue()) {
w.write(implName);
w.write(System.lineSeparator());
}
w.flush();
}
resourceProducer.produce(
new GeneratedResourceBuildItem(
"META-INF/services/" + serviceName,
os.toByteArray()
)
);
}
}
}
2.3.2.3. Itens de construção vazios
Os itens de construção vazios são classes finais (geralmente vazias) que estendem io.quarkus.builder.item.EmptyBuildItem
. Eles representam itens de construção que, na verdade, não contêm dados e permitem que esses itens sejam produzidos e consumidos sem a necessidade de instanciar classes vazias. Eles não podem ser instanciados.
Como não podem ser instanciados, não podem ser injetados por nenhum meio nem retornados por uma etapa de construção (ou por meio de um BuildProducer ). Para produzir um item de construção vazio, você deve anotar a etapa de construção com @Produce(MyEmptyBuildItem.class) e consumi-la com @Consume(MyEmptyBuildItem.class) .
|
public final class NativeImageBuildItem extends EmptyBuildItem {
// empty
}
Os itens de construção vazios podem representar "barreiras" que podem impor a ordem entre as etapas. Eles também podem ser usados da mesma forma que os sistemas de construção populares usam "pseudo-alvos", ou seja, o item de construção pode representar um objetivo conceitual que não tem uma representação concreta.
/**
* Contrived build step that produces the native image on disk. The main augmentation
* step (which is run by Maven or Gradle) would be declared to consume this empty item,
* causing this step to be run.
*/
@BuildStep
@Produce(NativeImageBuildItem.class)
void produceNativeImage() {
// ...
// (produce the native image)
// ...
}
/**
* This would always run after {@link #produceNativeImage()} completes, producing
* an instance of {@code SomeOtherBuildItem}.
*/
@BuildStep
@Consume(NativeImageBuildItem.class)
SomeOtherBuildItem secondBuildStep() {
return new SomeOtherBuildItem("foobar");
}
2.3.2.4. Itens de construção de Erro de Validação
Eles representam itens de construção com erros de validação que fazem a construção falhar. Estes itens de construção são consumidos durante a inicialização do contêiner CDI.
@BuildStep
void checkCompatibility(Capabilities capabilities, BuildProducer<ValidationErrorBuildItem> validationErrors) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)
&& capabilities.isPresent(Capability.RESTEASY)) {
validationErrors.produce(new ValidationErrorBuildItem(
new ConfigurationException("Cannot use both RESTEasy Classic and Reactive extensions at the same time")));
}
}
2.3.2.5. Itens de construção de Resultado de Artefato
Eles representam itens de construção que contêm o artefato executável gerado pela construção, como um uberjar ou thin jar. Esses itens de construção também podem ser usados para sempre executar uma etapa de construção sem a necessidade de produzir nada.
@BuildStep
@Produce(ArtifactResultBuildItem.class)
void runBuildStepThatProducesNothing() {
// ...
}
2.3.3. Injeção
As classes que contêm etapas de construção suportam os seguintes tipos de injeção:
-
Injeção de parâmetros do construtor
-
Injeção de campo
-
Injeção de parâmetros de métodos (apenas para métodos de etapas de construção)
As classes de etapa de construção são instanciadas e injetadas para cada invocação de etapa de construção e são descartadas posteriormente. O estado só deve ser comunicado entre as etapas de construção por meio de itens de construção, mesmo que as etapas estejam na mesma classe.
Os campos finais não são considerados para injeção, mas podem ser preenchidos por meio da injeção de parâmetros do construtor, se desejado. Os campos estáticos nunca são considerados para injeção. |
Os tipos de valores que podem ser injetados incluem:
-
Build items produced by previous build steps
-
Build producers to produce items for subsequent build steps
-
Configuration Mapping types
-
Template objects for bytecode recording
Os objetos que são injetados em um método de etapa de construção ou em sua classe não devem ser usados fora da execução desse método. |
A injeção é resolvida no momento da compilação por meio de um processador de anotações, e o código resultante não tem permissão para injetar campos privados ou invocar métodos privados. |
2.3.4. Produzindo valores
Uma etapa de construção pode produzir valores para as etapas subsequentes de várias maneiras possíveis:
-
By returning a simple build item or multi build item instance
-
Ao devolver um
List
de uma classe de item de construção múltipla -
Ao injetar um
BuildProducer
de uma classe de item simples ou de construção múltipla -
By annotating the method with
@io.quarkus.deployment.annotations.Produce
, giving the class name of an empty build item
Se um item de construção simples for declarado em uma etapa de construção, ele deverá ser produzido durante essa etapa de construção, caso contrário, ocorrerá um erro. Os produtores de construção, que são injetados nas etapas, não devem ser usados fora dessa etapa.
Observe que um método @BuildStep
só será chamado se produzir algo que outro consumidor ou o resultado final exija. Se não houver um consumidor para um determinado item, ele não será produzido. O que é necessário dependerá do destino final que está sendo produzido. Por exemplo, ao executar no modo de desenvolvedor, a saída final não solicitará itens de construção específicos do GraalVM, como ReflectiveClassBuildItem
, portanto, os métodos que produzem apenas esses itens não serão chamados.
2.3.5. Consumindo valores
Um passo de construção pode consumir valores de passos anteriores das seguintes formas:
-
By injecting a simple build item
-
Ao injetar um
Optional
de uma classe de item de construção simples -
By injecting a
List
of a multi build item class -
By annotating the method with
@io.quarkus.deployment.annotations.Consume
, giving the class name of an empty build item
Normalmente, é um erro uma etapa incluída consumir um item de construção simples que não é produzido por nenhuma outra etapa. Dessa forma, é garantido que todos os valores declarados estarão presentes e não null
quando uma etapa for executada.
Às vezes, um valor não é necessário para que a construção seja concluída, mas pode informar algum comportamento da etapa de construção se estiver presente. Nesse caso, o valor pode ser injetado opcionalmente.
Os valores de construção múltipla são sempre considerados opcionais. Se não estiverem presentes, será injetada uma lista vazia. |
2.3.5.1. Produção de valor fraco
Normalmente, uma etapa de construção é incluída sempre que produz qualquer item de construção que, por sua vez, é consumido por qualquer outra etapa de construção. Dessa forma, apenas as etapas necessárias para produzir o(s) artefato(s) final(is) são incluídas, e as etapas relacionadas a extensões que não estão instaladas ou que produzem apenas itens de construção que não são relevantes para o tipo de artefato em questão são excluídas.
Nos casos em que esse não é o comportamento desejado, a anotação @io.quarkus.deployment.annotations.Weak
pode ser usada. Essa anotação indica que a etapa de construção não deve ser incluída automaticamente apenas com base na produção do valor anotado.
/**
* This build step is only run if something consumes the ExecutorClassBuildItem.
*/
@BuildStep
void createExecutor(
@Weak BuildProducer<GeneratedClassBuildItem> classConsumer,
BuildProducer<ExecutorClassBuildItem> executorClassConsumer
) {
ClassWriter cw = new ClassWriter(Gizmo.ASM_API_VERSION);
String className = generateClassThatCreatesExecutor(cw); (1)
classConsumer.produce(new GeneratedClassBuildItem(true, className, cw.toByteArray()));
executorClassConsumer.produce(new ExecutorClassBuildItem(className));
}
1 | Este método (não fornecido neste exemplo) geraria a classe utilizando a API ASM. |
Certos tipos de itens de construção geralmente são sempre consumidos, como classes ou recursos gerados. Uma extensão pode produzir um item de construção junto com uma classe gerada para facilitar o uso desse item de construção. Essa etapa de construção usaria a anotação @Weak
no item de construção da classe gerada, enquanto produziria normalmente o outro item de construção. Se o outro item de construção for consumido por algo, a etapa será executada e a classe será gerada. Se nada consumir o outro item de construção, a etapa não será incluída no processo de construção.
No exemplo acima, GeneratedClassBuildItem
só seria produzido se ExecutorClassBuildItem
fosse consumido por alguma outra etapa de construção.
Note that when using bytecode recording, the implicitly generated class can be declared to be weak by
using the optional
attribute of the @io.quarkus.deployment.annotations.Record
annotation.
/**
* This build step is only run if something consumes the ExecutorBuildItem.
*/
@BuildStep
@Record(value = ExecutionTime.RUNTIME_INIT, optional = true) (1)
ExecutorBuildItem createExecutor( (2)
ExecutorRecorder recorder,
ThreadPoolConfig threadPoolConfig
) {
return new ExecutorBuildItem(
recorder.setupRunTime(
shutdownContextBuildItem,
threadPoolConfig,
launchModeBuildItem.getLaunchMode()
)
);
}
1 | Note o atributo optional . |
2 | This example is using recorder proxies; see the section on bytecode recording for more information. |
2.3.6. Arquivos de Aplicações
A anotação @BuildStep
também pode registrar arquivos de marcadores que determinam quais arquivos no caminho da classe são considerados "Arquivos de Aplicações" e, portanto, serão indexados. Isso é feito por meio do applicationArchiveMarkers
. Por exemplo, a extensão ArC registra META-INF/beans.xml
, o que significa que todos os arquivos no caminho da classe com um arquivo beans.xml
serão indexados.
2.3.7. Usando o Carregador de Classe de Contexto da Thread
A etapa de construção será executada com um TCCL que pode carregar classes de usuário da implantação de forma segura para o transformador. Esse carregador de classes dura apenas a vida útil da ampliação e é descartado depois. As classes serão carregadas novamente em um carregador de classes diferente no tempo de execução. Isso significa que carregar uma classe durante a ampliação não impede que ela seja transformada ao ser executada no modo de desenvolvimento/teste.
2.3.8. Adicionar JARs externos ao indexador com IndexDependencyBuildItem
O índice de classes verificadas não incluirá automaticamente suas dependências de classe externas. Para adicionar dependências, crie um @BuildStep
que produza objetos IndexDependencyBuildItem
, para um groupId
e artifactId
.
É importante especificar todos os artefatos necessários a serem adicionados ao indexador. Nenhum artefato é adicionado implicitamente de forma transitória. |
A extensão Amazon Alexa
adiciona bibliotecas dependentes do Alexa SDK que são utilizadas nas transformações Jackson JSON, para que as classes reflexivas sejam identificadas e incluídas em BUILD_TIME
.
@BuildStep
void addDependencies(BuildProducer<IndexDependencyBuildItem> indexDependency) {
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-runtime"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-lambda-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-servlet-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-dynamodb-persistence-adapter"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-apache-client"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model-runtime"));
}
Com os artefatos adicionados ao indexador Jandex
, você pode agora pesquisar o índice para identificar classes que implementam uma interface, subclasses de uma classe específica ou classes com uma anotação de alvo.
Por exemplo, a extensão Jackson
usa um código como o abaixo para pesquisar anotações usadas na desserialização de JSON e adicioná-las à hierarquia reflexiva para análise BUILD_TIME
.
DotName JSON_DESERIALIZE = DotName.createSimple(JsonDeserialize.class.getName());
IndexView index = combinedIndexBuildItem.getIndex();
// handle the various @JsonDeserialize cases
for (AnnotationInstance deserializeInstance : index.getAnnotations(JSON_DESERIALIZE)) {
AnnotationTarget annotationTarget = deserializeInstance.target();
if (CLASS.equals(annotationTarget.kind())) {
DotName dotName = annotationTarget.asClass().name();
Type jandexType = Type.create(dotName, Type.Kind.CLASS);
reflectiveHierarchyClass.produce(new ReflectiveHierarchyBuildItem(jandexType));
}
}
2.3.9. Visualizando dependências de etapas de construção
Ocasionalmente, pode ser útil ver uma representação visual das interações entre as várias etapas de construção. Nesses casos, adicionar -Dquarkus.builder.graph-output=build.dot
ao compilar um aplicativo resultará na criação do arquivo build.dot
no diretório raiz do projeto. Consulte isto para obter uma lista de softwares que podem abrir o arquivo e mostrar a representação visual real.
2.4. Configuração
A configuração no Quarkus é baseada no SmallRye Config. Todos os recursos fornecidos pelo SmallRye Config também estão disponíveis no Quarkus.
As extensões devem usar o @ConfigMapping do SmallRye Config para mapear a configuração exigida pela extensão. Isso permitirá que o Quarkus exponha automaticamente uma instância do mapeamento para cada fase de configuração e gere a documentação da configuração.
2.4.1. Fases de Configuração
Os mapeamentos de configuração são estritamente vinculados à fase de configuração, e a tentativa de acessar um mapeamento de configuração fora da fase correspondente resultará em um erro. Eles determinam quando as chaves contidas são lidas da configuração e quando estão disponíveis para as aplicações. As fases definidas pelo io.quarkus.runtime.annotations.ConfigPhase
são as seguintes:
Nome da fase | Lido e disponível em tempo de construção | Disponível em tempo de execução | Lido durante a inicialização estática | Relido durante a inicialização (executável nativo) | Notas |
---|---|---|---|---|---|
|
✓ |
✗ |
✗ |
✗ |
Adequado para coisas que afetam a construção. |
|
✓ |
✓ |
✗ |
✗ |
Apropriado para coisas que afetam a construção e devem ser visíveis para o código em tempo de execução. Não lido da configuração em tempo de execução. |
|
✗ |
✓ |
✓ |
✓ |
Não disponível na construção, lido no início em todos os modos. |
Para todos os casos, com exceção do caso BUILD_TIME
, a interface de mapeamento da configuração e todos os grupos e tipos de configuração nela contidos devem estar localizados ou ser acessíveis a partir do artefato de tempo de execução da extensão. Os mapeamentos de configuração da fase BUILD_TIME
podem estar localizados ou acessíveis a partir de qualquer um dos artefatos de tempo de execução ou de implantação da extensão.
2.4.2. Exemplo de Configuração
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import java.io.File;
import java.util.logging.Level;
/**
* Logging configuration.
*/
@ConfigMapping(prefix = "quarkus.log") (1)
@ConfigRoot(phase = ConfigPhase.RUN_TIME) (2)
public interface LogConfiguration {
// ...
/**
* Configuration properties for the logging file handler.
*/
FileConfig file();
interface FileConfig {
/**
* Enable logging to a file.
*/
@WithDefault("true")
boolean enable();
/**
* The log format.
*/
@WithDefault("%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%c{1.}] (%t) %s%e%n")
String format();
/**
* The level of logs to be written into the file.
*/
@WithDefault("ALL")
Level level();
/**
* The name of the file in which logs will be written.
*/
@WithDefault("application.log")
File path();
}
}
public class LoggingProcessor {
// ...
/*
* Logging configuration.
*/
LogConfiguration config; (3)
}
Um nome de propriedade de configuração pode ser dividido em segmentos. Por exemplo, um nome de propriedade como quarkus.log.file.enable
pode ser dividido nos seguintes segmentos:
-
quarkus
- um namespace reivindicado pelo Quarkus que é um prefixo para as interfaces@ConfigMapping
, -
log
- um segmento de nome que corresponde ao prefixo definido na interface anotada com@ConfigMapping
, -
file
- um segmento de nome que corresponde ao campofile
desta classe, -
enable
- um segmento de nome que corresponde ao campoenable
emFileConfig
.
1 | A anotação @ConfigMapping indica que a interface é um mapeamento de configuração, nesse caso, um que corresponde a um segmento quarkus.log . |
2 | A anotação @ConfigRoot indica qual fase de configuração a configuração se aplica. |
3 | Aqui, o LoggingProcessor injeta uma instância LogConfiguration automaticamente ao detectar a anotação @ConfigRoot . |
Um application.properties
correspondente para o exemplo acima poderia ser:
quarkus.log.file.enable=true
quarkus.log.file.level=DEBUG
quarkus.log.file.path=/tmp/debug.log
Uma vez que format
não está definido nestas propriedades, será utilizado o valor padrão de @WithDefault
.
A configuration mapping name can contain an extra suffix segment for the case where there are configuration
mappings for multiple Fases de Configuração. Classes which correspond to the BUILD_TIME
and BUILD_AND_RUN_TIME_FIXED
may end with BuildTimeConfig
or BuildTimeConfiguration
, classes which correspond to the RUN_TIME
phase
may end with RuntimeConfig
, RunTimeConfig
, RuntimeConfiguration
or RunTimeConfiguration
.
2.4.3. Documentação de Referência de Configuração
A configuração é uma parte importante de cada extensão e, portanto, precisa ser documentada adequadamente. Cada propriedade de configuração deve ter um comentário Javadoc adequado.
Embora seja útil ter a documentação disponível durante a codificação, a documentação de configuração também deve estar disponível nos guias de extensão. A construção do Quarkus gera automaticamente a documentação de configuração com base nos comentários do Javadoc, mas ela precisa ser incluída explicitamente em cada guia.
2.4.3.1. Escrevendo a documentação
Cada propriedade de configuração requer um Javadoc que explique o seu objetivo.
A primeira frase deve ser significativa e autônoma, uma vez que está incluída no sumário. |
Embora os comentários Javadoc padrão sejam perfeitamente adequados para documentação simples (até mesmo recomendados), o AsciiDoc é mais adequado para dicas, extratos de código-fonte, listas e muito mais:
/**
* Class name of the Hibernate ORM dialect. The complete list of bundled dialects is available in the
* https://docs.jboss.org/hibernate/stable/orm/javadocs/org/hibernate/dialect/package-summary.html[Hibernate ORM JavaDoc].
*
* [NOTE]
* ====
* Not all the dialects are supported in GraalVM native executables: we currently provide driver extensions for
* PostgreSQL, MariaDB, Microsoft SQL Server and H2.
* ====
*
* @asciidoclet
*/
Optional<String> dialect();
Para usar o AsciiDoc, o comentário do Javadoc deve ser anotado com a tag @asciidoclet
. Essa tag tem duas finalidades: é usada como um marcador para a ferramenta de geração Quarkus, mas também é usada pelo processo javadoc
para a geração do Javadoc.
Um exemplo mais detalhado:
// @formatter:off
/**
* Name of the file containing the SQL statements to execute when Hibernate ORM starts.
* Its default value differs depending on the Quarkus launch mode:
*
* * In dev and test modes, it defaults to `import.sql`.
* Simply add an `import.sql` file in the root of your resources directory
* and it will be picked up without having to set this property.
* Pass `no-file` to force Hibernate ORM to ignore the SQL import file.
* * In production mode, it defaults to `no-file`.
* It means Hibernate ORM won't try to execute any SQL import file by default.
* Pass an explicit value to force Hibernate ORM to execute the SQL import file.
*
* If you need different SQL statements between dev mode, test (`@QuarkusTest`) and in production, use Quarkus
* https://quarkus.io/guides/config#configuration-profiles[configuration profiles facility].
*
* [source,property]
* .application.properties
* ----
* %dev.quarkus.hibernate-orm.sql-load-script = import-dev.sql
* %test.quarkus.hibernate-orm.sql-load-script = import-test.sql
* %prod.quarkus.hibernate-orm.sql-load-script = no-file
* ----
*
* [NOTE]
* ====
* Quarkus supports `.sql` file with SQL statements or comments spread over multiple lines.
* Each SQL statement must be terminated by a semicolon.
* ====
*
* @asciidoclet
*/
// @formatter:on
Optional<String> sqlLoadScript();
Para que a identação seja respeitada no comentário do Javadoc (itens de lista espalhados em várias linhas ou código-fonte recuado), o formatador automático do Eclipse deve ser desativado (o formatador é incluído automaticamente na construção), com os marcadores // @formatter:off
/ // @formatter:on
. Isso exige comentários separados e um espaço obrigatório após o marcador //
.
Os blocos abertos ( |
Por padrão, o gerador de documentação usará o nome do campo hifenizado como a chave de um
É possível escrever uma explicação textual para o valor padrão da documentação, o que é útil quando ela é gerada:
|
2.4.3.2. Escrevendo documentação de seção
Para gerar uma seção de configuração de um determinado grupo, use a anotação @ConfigDocSection
:
/**
* Config group related configuration.
* Amazing introduction here
*/
@ConfigDocSection (1)
ConfigGroupConfig configGroup();
1 | Isso adicionará uma seção de documentação para o item de configuração configGroup na documentação gerada. O título e a introdução da seção serão derivados do javadoc do item de configuração. A primeira frase do javadoc é considerada como o título da seção e as frases restantes são usadas como a introdução da seção. |
2.4.3.3. Gerando a documentação
Para gerar a documentação:
-
Execute
./mvnw -DquicklyDocs
-
Pode ser executado globalmente ou num diretório de extensão específico (por exemplo,
extensions/mailer
).
A documentação é gerada na target/asciidoc/generated/config/
global localizada na raiz do projeto.
2.4.3.4. Incluindo a documentação no guia de extensão
Para incluir a documentação de referência de configuração gerada num guia, use:
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
Para incluir apenas um grupo de configuração específico:
include::{generated-dir}/hyphenated-config-group-class-name-with-runtime-or-deployment-namespace-replaced-by-config-group-namespace.adoc[opts=optional, leveloffset=+1]
Por exemplo, o grupo de configuração io.quarkus.vertx.http.runtime.FormAuthConfig
será gerado em um arquivo chamado quarkus-vertx-http-config-group-form-auth-config.adoc
.
Algumas recomendações:
-
opts=optional
é obrigatório para não falhar a construção se apenas parte da documentação de configuração tiver sido gerada. -
A documentação é gerada com um nível de título de 2 (ou seja,
==
). Pode ser necessário um ajuste comleveloffset=+N
. -
A documentação completa de configuração não deve ser incluída no meio do guia.
Se o guia incluir um exemplo de application.properties
, uma dica deve ser incluída logo abaixo do trecho de código:
[TIP]
For more information about the extension configuration please refer to the <<configuration-reference,Configuration Reference>>.
E no final do guia, a extensa documentação de configuração:
[[configuration-reference]]
== Configuration Reference
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
Toda a documentação deve ser gerada e validada antes de ser aceita. |
2.5. Inclusão Condicional de Etapas
É possível incluir um determinado @BuildStep
somente em determinadas condições. A anotação @BuildStep
tem dois parâmetros opcionais: onlyIf
e onlyIfNot
. Esses parâmetros podem ser definidos para uma ou mais classes que implementam BooleanSupplier
. A etapa de construção só será incluída quando o método retornar true
(para onlyIf
) ou false
(para onlyIfNot
).
The condition class can inject configuration mappings as long as they belong to a build-time phase. Run time configuration is not available for condition classes.
A classe de condição também pode injetar um valor do tipo io.quarkus.runtime.LaunchMode
. Há suporte para injeção de campo e parâmetro de construtor.
@BuildStep(onlyIf = IsDevMode.class)
LogCategoryBuildItem enableDebugLogging() {
return new LogCategoryBuildItem("org.your.quarkus.extension", Level.DEBUG);
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
If you need to make your build step conditional on the presence or absence of another extension, you can use Capacidades for that. |
Também é possível aplicar um conjunto de condições a todos os passos de construção numa determinada classe com @BuildSteps
:
@BuildSteps(onlyIf = MyDevModeProcessor.IsDevMode.class) (1)
class MyDevModeProcessor {
@BuildStep
SomeOutputBuildItem mainBuildStep(SomeOtherBuildItem input) { (2)
return new SomeOutputBuildItem(input.getValue());
}
@BuildStep
SomeOtherOutputBuildItem otherBuildStep(SomeOtherInputBuildItem input) { (3)
return new SomeOtherOutputBuildItem(input.getValue());
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
}
1 | Esta condição será aplicada a todos os métodos definidos em MyDevModeProcessor |
2 | A etapa principal de construção só será executada no modo de desenvolvimento. |
3 | A outra etapa de construção só será executada no modo de desenvolvimento. |
2.6. Generating Bytecode
2.6.1. Gravação de Bytecode
Um dos principais resultados do processo de construção é o bytecode gravado. Na verdade, esse bytecode configura o ambiente de tempo de execução. Por exemplo, para iniciar o Undertow, a aplicação resultante terá algum bytecode que registra diretamente todas as instâncias de Servlet e, em seguida, inicia o Undertow.
Como escrever bytecode diretamente é complexo, isso é feito por meio de gravadores de bytecode. No momento da implantação, as invocações são feitas em objetos gravadores que contêm a lógica real do tempo de execução, mas, em vez de essas invocações prosseguirem normalmente, elas são interceptadas e gravadas (daí o nome). Essa gravação é então usada para gerar bytecode que executa a mesma sequência de invocações no tempo de execução. Essencialmente, essa é uma forma de execução diferida em que as invocações feitas no momento da implantação são adiadas até o tempo de execução.
Vejamos o exemplo clássico do tipo 'Olá Mundo'. Para fazer isto à maneira do Quarkus, criaríamos um gravador como se segue:
@Recorder
class HelloRecorder {
public void sayHello(String name) {
System.out.println("Hello" + name);
}
}
E, em seguida, criaríamos um passo de construção que utilize este gravador:
@Record(RUNTIME_INIT)
@BuildStep
public void helloBuildStep(HelloRecorder recorder) {
recorder.sayHello("World");
}
Quando essa etapa de construção é executada, nada é impresso no console. Isso ocorre porque o HelloRecorder
que é injetado é, na verdade, um proxy que registra todas as invocações. Em vez disso, se executarmos o programa Quarkus resultante, veremos "Hello World" impresso no console.
Os métodos em um gravador podem retornar um valor, que deve ser proxiable (se você quiser retornar um item não proxiable, envolva-o em io.quarkus.runtime.RuntimeValue
). Esses proxies não podem ser invocados diretamente, mas podem ser passados para outros métodos do gravador. Esse pode ser qualquer método de gravador, inclusive de outros métodos @BuildStep
, portanto, um padrão comum é produzir instâncias BuildItem
que envolvam os resultados dessas invocações de gravadores.
Por exemplo, para fazer alterações arbitrárias em uma implantação de Servlet, o Undertow tem um ServletExtensionBuildItem
, que é um MultiBuildItem
que envolve uma instância de ServletExtension
. Posso retornar um ServletExtension
de um gravador em outro módulo, e o Undertow o consumirá e o passará para o método do gravador que inicia o Undertow.
No tempo de execução, o bytecode será chamado na ordem em que foi gerado. Isso significa que as dependências da etapa de construção controlam implicitamente a ordem em que o bytecode gerado é executado. No exemplo acima, sabemos que o bytecode que produz um ServletExtensionBuildItem
será executado antes do bytecode que o consome.
Os seguintes objetos podem ser passados aos gravadores:
-
Primitivos
-
String
-
Objetos Class<?>
-
Objetos retornados de uma invocação anterior do gravador
-
Objetos com um construtor sem argumentos e getter/setters para todas as propriedades (ou campos públicos)
-
Objetos com um construtor anotado com
@RecordableConstructor
com nomes de parâmetros que correspondem a nomes de campos -
Qualquer objeto arbitrário através do mecanismo
io.quarkus.deployment.recording.RecorderContext#registerSubstitution(Class, Class, Class)
-
Vetores, Listas e Mapas dos elementos acima
Nos casos em que alguns campos de um objeto a ser gravado devam ser ignorados (ou seja, o valor que será construído não deve ser refletido em tempo de execução), o Se a classe não puder depender do Quarkus, o Quarkus pode utilizar qualquer anotação personalizada, desde que a extensão implemente o SPI Este mesmo SPI também pode ser utilizado para fornecer uma anotação personalizada que substituirá |
2.6.2. Injetando Configuração nos Gravadores
Os objetos de configuração com a fase RUNTIME
ou BUILD_AND_RUNTIME_FIXED
podem ser injetados nos gravadores por meio da injeção de construtor. Basta criar um construtor que receba os objetos de configuração de que o gravador precisa. Se o gravador tiver vários construtores, o usuário poderá anotar aquele que deseja que o Quarkus use com @Inject
. Se o gravador quiser injetar a configuração de tempo de execução, mas também for usado no momento da inicialização estática, será necessário injetar um RuntimeValue<ConfigObject>
. Esse valor só será definido quando os métodos de tempo de execução estiverem sendo invocados.
2.6.3. RecorderContext
io.quarkus.deployment.recording.RecorderContext
fornece alguns métodos de conveniência para aprimorar a gravação de bytecode, o que inclui a capacidade de registrar funções de criação para classes sem construtores sem argumento, registrar uma substituição de objeto (basicamente um transformador de um objeto não serializável para um serializável e vice-versa) e criar um proxy de classe. Essa interface pode ser injetada diretamente como um parâmetro de método em qualquer método @Record
.
Chamar classProxy
com um determinado nome de classe totalmente qualificado criará uma instância de Class
que pode ser passada para um método de gravador e, no tempo de execução, será substituída pela classe cujo nome foi passado para classProxy()
. No entanto, esse método não deve ser necessário na maioria dos casos de uso, pois é seguro carregar diretamente as classes de implantação/aplicação no tempo de processamento nas etapas de construção. Portanto, esse método está obsoleto. No entanto, há alguns casos de uso em que esse método é útil, como a referência a classes que foram geradas em etapas de construção anteriores usando GeneratedClassBuildItem
.
2.6.3.1. Imprimindo o tempo de execução da etapa
Às vezes, pode ser útil saber o tempo exato que cada tarefa de inicialização (que é o resultado de cada gravação de bytecode) leva quando a aplicação é executada. A maneira mais simples de determinar essas informações é iniciar o aplicativo Quarkus com a propriedade de sistema -Dquarkus.debug.print-startup-times=true
. O resultado será algo parecido com:
Build step LoggingResourceProcessor.setupLoggingRuntimeInit completed in: 42ms
Build step ConfigGenerationBuildStep.checkForBuildTimeConfigChange completed in: 4ms
Build step SyntheticBeansProcessor.initRuntime completed in: 0ms
Build step ConfigBuildStep.validateConfigProperties completed in: 1ms
Build step ResteasyStandaloneBuildStep.boot completed in: 95ms
Build step VertxHttpProcessor.initializeRouter completed in: 1ms
Build step VertxHttpProcessor.finalizeRouter completed in: 4ms
Build step LifecycleEventsBuildStep.startupEvent completed in: 1ms
Build step VertxHttpProcessor.openSocket completed in: 93ms
Build step ShutdownListenerBuildStep.setupShutdown completed in: 1ms
2.6.4. Using Gizmo
In some scenarios, more significant manipulation of bytecode may be needed. If bytecode recording isn’t sufficient, Gizmo is a convenient alternative to ASM, with a higher-level API.
2.6.5. Verificação do Classpath de Tempo de Execução
As extensões geralmente precisam de uma maneira de determinar se uma classe faz parte do classpath de tempo de execução da aplicação. A maneira correta de uma extensão realizar essa verificação é usar io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime
.
2.7. Contextos e Injeção de Dependência
The CDI integration guide has more detail on common CDI-related use cases, and example code for solutions.
2.7.1. Pontos de Extensão
Como um tempo de execução baseado em CDI, as extensões do Quarkus geralmente disponibilizam beans CDI como parte do comportamento da extensão. No entanto, a solução de DI do Quarkus não suporta Extensões Portáteis CDI. Em vez disso, as extensões do Quarkus podem fazer uso de vários Pontos de Extensão de Tempo de Construção.
2.8. Quarkus Dev UI
You can make your extension support the Quarkus Dev UI for a greater developer experience.
2.9. Endpoints definidos pela extensão
Sua extensão pode adicionar endpoints adicionais que não sejam de aplicações para serem servidos juntamente com endpoints para Health, Metrics, OpenAPI, Swagger UI, etc.
Use um NonApplicationRootPathBuildItem
para definir um endpoint:
@BuildStep
RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.route("custom-endpoint")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
Observe que o caminho acima não começa com um '/', o que indica que é um caminho relativo. O endpoint acima será servido em relação à raiz configurada do endpoint sem aplicação. A raiz do endpoint que não é de aplicação é /q
por padrão, o que significa que o endpoint resultante será encontrado em /q/custom-endpoint
.
Os caminhos absolutos são tratados de forma diferente. Se o acima chamou route("/custom-endpoint")
, o endpoint resultante será encontrado em /custom-endpoint
.
Se uma extensão precisar de endpoints não relacionados com a aplicação aninhados:
@BuildStep
RouteBuildItem myNestedExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.nestedRoute("custom-endpoint", "deep")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
Dada uma raiz padrão de endpoint não relacionado a aplicação de /q
, isto criará um endpoint em /q/custom-endpoint/deep
.
Os caminhos absolutos também têm impacto sobre os endpoints aninhados. Se o usuário acima chamou nestedRoute("custom-endpoint", "/deep")
, o endpoint resultante será encontrado em /deep
.
Refer to the Quarkus Vertx HTTP configuration reference for details on how the non-application root path is configured.
2.10. Verificação de Integridade da Extensão
As verificações de integridade são fornecidas através da extensão quarkus-smallrye-health
. Ela fornece capacidades de verificação de vivacidade e prontidão.
Ao escrever uma extensão, é vantajoso fornecer verificações de integridade para a extensão, que podem ser automaticamente incluídas sem que o programador tenha de escrever os seus próprios controles.
Para prover uma verificação de integridade, você deve fazer o seguinte:
-
Importe a extensão
quarkus-smallrye-health
como uma dependência opcional em seu módulo de tempo de execução para que ela não afete o tamanho da aplicação se a verificação de integridade não for incluída. -
Crie sua verificação de integridade seguindo o guia SmallRye Health. Recomendamos que você forneça apenas uma verificação de prontidão para uma extensão (a verificação de vivacidade foi criada para expressar o fato de que um aplicativo está ativo e precisa ser leve).
-
Importe a biblioteca
quarkus-smallrye-health-spi
no seu módulo de implantação. -
Adicione uma etapa de construção no seu módulo de implantação que produz um
HealthBuildItem
. -
Adicione uma forma de desativar a verificação de integridade da extensão através de um item de configuração
quarkus.<extension>.health.enabled
que deve estar ativado por padrão.
Segue um exemplo da extensão Agroal que fornece um DataSourceHealthCheck
para validar a prontidão de uma fonte de dados.
@BuildStep
HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) {
return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck",
agroalBuildTimeConfig.healthEnabled);
}
2.11. Métricas de Extensão
A extensão quarkus-micrometer
e a extensão quarkus-smallrye-metrics
oferecem suporte à coleta de métricas. Como uma nota de compatibilidade, a extensão quarkus-micrometer
adapta a API MP Metrics aos primitivos da biblioteca Micrometer, de modo que a extensão quarkus-micrometer
pode ser ativada sem quebrar o código que depende da API MP Metrics. Observe que as métricas emitidas pelo Micrometer são diferentes; consulte a documentação da extensão quarkus-micrometer
para obter mais informações.
A camada de compatibilidade para as APIs de Métricas MP será transferida para uma extensão diferente no futuro. |
Existem dois padrões gerais que as extensões podem utilizar para interagir com uma extensão de métricas opcional para adicionar as suas próprias métricas:
-
Padrão Consumidor: Uma extensão declara um
MetricsFactoryConsumerBuildItem
e usa-o para fornecer um gravador de bytecode para a extensão de métricas. Quando a extensão de métricas for inicializada, ela irá iterar sobre os consumidores registrados para inicializá-los com umMetricsFactory
. Essa fábrica pode ser usada para declarar métricas agnósticas à API, o que pode ser uma boa opção para extensões que fornecem um objeto instrumentável para coletar estatísticas (por exemplo, a classeStatistics
do Hibernate). -
Padrão Binder: Uma extensão pode optar por usar implementações de coleta completamente diferentes, dependendo do sistema de métricas. Um parâmetro da etapa de construção
Optional<MetricsCapabilityBuildItem> metricsCapability
pode ser usado para declarar ou inicializar métricas específicas da API com base na extensão de métricas ativa (por exemplo, "smallrye-metrics" ou "micrometer"). Este padrão pode ser combinado com o padrão Consumidor utilizandoMetricsFactory::metricsSystemSupported()
para testar a extensão de métrica ativa dentro do gravador.
Lembre-se de que o suporte para métricas é opcional. As extensões podem usar um parâmetro Optional<MetricsCapabilityBuildItem> metricsCapability
em sua etapa de construção para testar a presença de uma extensão de métricas habilitada. Considere o uso de configuração adicional para controlar o comportamento das métricas. As métricas de fontes de dados podem ser caras, por exemplo, portanto, sinalizadores de configuração adicionais são usados para habilitar a coleta de métricas em fontes de dados individuais.
Ao adicionar métricas para a sua extensão, você pode se encontrar em uma das seguintes situações:
-
Uma biblioteca subjacente utilizada pela extensão está usando diretamente uma API de métricas específica (MP Metrics, Micrometer ou outra).
-
Uma biblioteca subjacente usa o seu próprio mecanismo de coleta de métricas e as disponibiliza em tempo de execução utilizando a sua própria API, por exemplo, a classe
Statistics
do Hibernate ou a Vert.xMetricsOptions
. -
Uma biblioteca subjacente não fornece métricas (ou não existe qualquer biblioteca) e você pretende adicionar instrumentação.
2.11.1. Caso 1: A biblioteca utiliza diretamente uma biblioteca de métricas
Se a biblioteca utilizar diretamente uma API de métricas, existem duas opções:
-
Use um parâmetro
Optional<MetricsCapabilityBuildItem> metricsCapability
para testar qual API de métricas é suportada (por exemplo, "smallrye-metrics" ou "micrometer") em sua etapa de construção e use isso para declarar ou inicializar seletivamente beans ou itens de construção específicos da API. -
Crie uma etapa de construção separada que consuma um
MetricsFactory
e utilize o métodoMetricsFactory::metricsSystemSupported()
no gravador de bytecode para inicializar os recursos necessários se a API de métricas pretendida for suportada (por exemplo, "smallrye-metrics" ou "micrometer").
As extensões podem precisar fornecer uma alternativa se não existir uma extensão de métricas ativa ou se a extensão não suportar a API exigida pela biblioteca.
2.11.2. Caso 2: A biblioteca fornece a sua própria API métrica
Existem dois exemplos de uma biblioteca que fornece a sua própria API de métricas:
-
A extensão define um objeto instrumentável como faz o Agroal com
io.agroal.api.AgroalDataSourceMetrics
, ou -
A extensão fornece a sua própria abstração de métricas, tal como Jaeger faz com
io.jaegertracing.spi.MetricsFactory
.
2.11.2.1. Observação de objetos instrumentáveis
Vejamos primeiro o caso do objeto instrumentável ( io.agroal.api.AgroalDataSourceMetrics
). Neste caso, você pode fazer o seguinte:
-
Defina um
BuildStep
que produz umMetricsFactoryConsumerBuildItem
que usa um GravadorRUNTIME_INIT
ouSTATIC_INIT
para definir um consumidorMetricsFactory
. Por exemplo, o seguinte cria umMetricsFactoryConsumerBuildItem
se e apenas se as métricas estiverem ativadas para o Agroal em geral e para uma fonte de dados especificamente:@BuildStep @Record(ExecutionTime.RUNTIME_INIT) void registerMetrics(AgroalMetricsRecorder recorder, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, BuildProducer<MetricsFactoryConsumerBuildItem> datasourceMetrics, List<AggregatedDataSourceBuildTimeConfigBuildItem> aggregatedDataSourceBuildTimeConfigs) { for (AggregatedDataSourceBuildTimeConfigBuildItem aggregatedDataSourceBuildTimeConfig : aggregatedDataSourceBuildTimeConfigs) { // Create a MetricsFactory consumer to register metrics for a data source // IFF metrics are enabled globally and for the data source // (they are enabled for each data source by default if they are also enabled globally) if (dataSourcesBuildTimeConfig.metricsEnabled && aggregatedDataSourceBuildTimeConfig.getJdbcConfig().enableMetrics.orElse(true)) { datasourceMetrics.produce(new MetricsFactoryConsumerBuildItem( recorder.registerDataSourceMetrics(aggregatedDataSourceBuildTimeConfig.getName()))); } } }
-
O gravador associado deve usar o
MetricsFactory
fornecido para registrar métricas. Para o Agroal, isto significa usar a APIMetricFactory
para observar os métodosio.agroal.api.AgroalDataSourceMetrics
. Por exemplo:/* RUNTIME_INIT */ public Consumer<MetricsFactory> registerDataSourceMetrics(String dataSourceName) { return new Consumer<MetricsFactory>() { @Override public void accept(MetricsFactory metricsFactory) { String tagValue = DataSourceUtil.isDefault(dataSourceName) ? "default" : dataSourceName; AgroalDataSourceMetrics metrics = getDataSource(dataSourceName).getMetrics(); // When using MP Metrics, the builder uses the VENDOR registry by default. metricsFactory.builder("agroal.active.count") .description( "Number of active connections. These connections are in use and not available to be acquired.") .tag("datasource", tagValue) .buildGauge(metrics::activeCount); ....
O MetricsFactory
fornece um construtor fluido para o registro de métricas, com o passo final para construir medidores ou contadores com base num Supplier
ou ToDoubleFunction
. Os temporizadores podem envolver implementações Callable
, Runnable
ou Supplier
, ou podem utilizar um TimeRecorder
para acumular pedaços de tempo. A extensão de métrica subjacente criará artefatos apropriados para observar ou medir as funções definidas.
2.11.2.2. Usando uma implementação específica da API de Métricas
Usar implementações específicas da API de Métricas pode ser preferível em alguns casos. O Jaeger, por exemplo, define a sua própria interface de métricas, io.jaegertracing.spi.MetricsFactory
, que usa para definir contadores e medidores. Um mapeamento direto dessa interface para o sistema de métricas será o mais eficiente. Nesse caso, é importante isolar essas implementações especializadas e evitar o carregamento antecipado de classes para garantir que a API de métricas permaneça uma dependência opcional em tempo de compilação.
Optional<MetricsCapabilityBuildItem> metricsCapability
podem ser usados na etapa de construção para controlar seletivamente a inicialização de beans ou a produção de outros itens de construção. A extensão Jaeger, por exemplo, pode usar o seguinte para controlar a inicialização de adaptadores especializados da API de Métricas:
+
/* RUNTIME_INIT */
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void setupTracer(JaegerDeploymentRecorder jdr, JaegerBuildTimeConfig buildTimeConfig, JaegerConfig jaeger,
ApplicationConfig appConfig, Optional<MetricsCapabilityBuildItem> metricsCapability) {
// Indicates that this extension would like the SSL support to be enabled
extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.JAEGER.getName()));
if (buildTimeConfig.enabled) {
// To avoid dependency creep, use two separate recorder methods for the two metrics systems
if (buildTimeConfig.metricsEnabled && metricsCapability.isPresent()) {
if (metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
jdr.registerTracerWithMicrometerMetrics(jaeger, appConfig);
} else {
jdr.registerTracerWithMpMetrics(jaeger, appConfig);
}
} else {
jdr.registerTracerWithoutMetrics(jaeger, appConfig);
}
}
}
Um gravador que consome um MetricsFactory
pode usar MetricsFactory::metricsSystemSupported()
pode ser usado para controlar a inicialização de objetos de métricas durante a gravação de bytecode de forma semelhante.
2.11.3. Caso 3: É necessário coletar métricas no código da extensão
Para definir suas próprias métricas do zero, você tem duas opções básicas: Usar os construtores genéricos do MetricFactory
ou seguir o padrão binder e criar instrumentação específica para a extensão de métricas habilitada.
Para utilizar a API MetricFactory
agnóstica em termos de extensão, o seu processador pode definir um BuildStep
que produz um MetricsFactoryConsumerBuildItem
que usa um Gravador RUNTIME_INIT
ou STATIC_INIT
para definir um consumidor MetricsFactory
.
+
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
MetricsFactoryConsumerBuildItem registerMetrics(MyExtensionRecorder recorder) {
return new MetricsFactoryConsumerBuildItem(recorder.registerMetrics());
}
+ - O registrador associado deve usar o MetricsFactory
fornecido para registrar métricas, por exemplo
+
final LongAdder extensionCounter = new LongAdder();
/* RUNTIME_INIT */
public Consumer<MetricsFactory> registerMetrics() {
return new Consumer<MetricsFactory>() {
@Override
public void accept(MetricsFactory metricsFactory) {
metricsFactory.builder("my.extension.counter")
.buildGauge(extensionCounter::longValue);
....
Lembre-se de que as extensões de métricas são opcionais. Mantenha a inicialização relacionada a métricas isolada de outras configurações para sua extensão e estruture seu código para evitar importações antecipadas de APIs de métricas. A coleta de métricas também pode ser cara. Considere o uso de configuração adicional específica da extensão para controlar o comportamento das métricas se a presença/ausência de suporte a métricas não for suficiente.
2.12. Personalizando tratamento JSON a partir de uma extensão
As extensões necessitam frequentemente de registrar serializadores e/ou desserializadores para os tipos que a extensão fornece.
Para isso, as extensões Jackson e JSON-B oferecem uma maneira de registrar o serializador/desserializador de dentro de um módulo de implantação de extensão.
Lembre-se que nem todo mundo vai precisar de JSON, por isso deve torná-lo opcional.
Se uma extensão pretende fornecer personalização relacionada a JSON, é altamente recomendável fornecer personalização para Jackson e JSON-B.
2.12.1. Personalizando Jackson
Primeiro, adicione uma dependência opcional a quarkus-jackson
no módulo de tempo de execução da sua extensão.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
<optional>true</optional>
</dependency>
Em seguida, crie um serializador ou um desserializador (ou ambos) para a Jackson, um exemplo do qual pode ser visto na extensão mongodb-panache
.
public class ObjectIdSerializer extends StdSerializer<ObjectId> {
public ObjectIdSerializer() {
super(ObjectId.class);
}
@Override
public void serialize(ObjectId objectId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (objectId != null) {
jsonGenerator.writeString(objectId.toString());
}
}
}
Adicione uma dependência a quarkus-jackson-spi
no módulo de implantação da sua extensão.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-spi</artifactId>
</dependency>
Adicione uma etapa de construção ao seu processador para registrar um módulo Jackson por meio do JacksonModuleBuildItem
. Você precisa nomear o seu módulo de uma forma única em todos os módulos Jackson.
@BuildStep
JacksonModuleBuildItem registerJacksonSerDeser() {
return new JacksonModuleBuildItem.Builder("ObjectIdModule")
.add(io.quarkus.mongodb.panache.jackson.ObjectIdSerializer.class.getName(),
io.quarkus.mongodb.panache.jackson.ObjectIdDeserializer.class.getName(),
ObjectId.class.getName())
.build();
}
A extensão Jackson utilizará então o item de construção produzido para registrar automaticamente um módulo no Jackson.
Se precisar de mais recursos de personalização do que o registro de um módulo, você pode produzir um bean CDI que implemente o io.quarkus.jackson.ObjectMapperCustomizer
por meio de um AdditionalBeanBuildItem
. Mais informações sobre a personalização da Jackson podem ser encontradas no guia JSON Configurando Suporte a JSON
2.12.2. Personalizando JSON-B
Primeiro, adicione uma dependência opcional a quarkus-jsonb
no módulo de tempo de execução da sua extensão.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
<optional>true</optional>
</dependency>
Em seguida, crie um serializador e/ou desserializador para JSON-B, um exemplo do qual pode ser visto na extensão mongodb-panache
.
public class ObjectIdSerializer implements JsonbSerializer<ObjectId> {
@Override
public void serialize(ObjectId obj, JsonGenerator generator, SerializationContext ctx) {
if (obj != null) {
generator.write(obj.toString());
}
}
}
Adicione uma dependência a quarkus-jsonb-spi
no módulo de implantação da sua extensão.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb-spi</artifactId>
</dependency>
Adicione um passo de construção ao seu processador para registrar o serializador através do JsonbSerializerBuildItem
.
@BuildStep
JsonbSerializerBuildItem registerJsonbSerializer() {
return new JsonbSerializerBuildItem(io.quarkus.mongodb.panache.jsonb.ObjectIdSerializer.class.getName()));
}
A extensão JSON-B utilizará então o item de construção produzido para registrar automaticamente o seu serializador/desserializador.
If you need more customization capabilities than registering a serializer or a deserializer,
you can produce a CDI bean that implements io.quarkus.jsonb.JsonbConfigCustomizer
via an AdditionalBeanBuildItem
.
More info about customizing JSON-B can be found on the JSON guide Configuring JSON support
2.13. Integração com o Modo de Desenvolvimento
Existem várias APIS que você pode usar para integrar com o modo de desenvolvimento e para obter informações sobre o estado atual.
2.13.1. Lidando com reinicializações
Quando o Quarkus está iniciando, é garantido que o site io.quarkus.deployment.builditem.LiveReloadBuildItem
esteja presente e forneça informações sobre esse início, em particular:
-
É uma inicialização limpa ou um recarregamento ao vivo
-
Se se tratar de um recarregamento ao vivo onde arquivos/classes alterados ativaram o recarregamento
Ele também fornece um mapa de contexto global que pode ser usado para armazenar informações entre reinicializações, sem a necessidade de recorrer a campos estáticos.
2.13.2. Ativando o Recarregamento ao Vivo
O recarregamento ao vivo geralmente é acionado por uma requisição HTTP, mas nem todas as aplicações são aplicações HTTP e algumas extensões podem querer acionar o recarregamento ao vivo com base em outros eventos. Para isso, você precisa implementar o io.quarkus.dev.spi.HotReplacementSetup
no seu módulo de tempo de execução e adicionar um META-INF/services/io.quarkus.dev.spi.HotReplacementSetup
que liste a sua implementação.
Na inicialização, o método setupHotDeployment
será chamado, e você pode usar o io.quarkus.dev.spi.HotReplacementContext
fornecido para iniciar uma varredura de arquivos alterados.
2.13.3. Serviços de Desenvolvimento
Where extensions use an external service, adding a Dev Service can improve the user experience in development and test modes. See how to write a Dev Service for more details.
2.14. Testando Extensões
O teste das extensões do Quarkus deve ser feito com a extensão io.quarkus.test.QuarkusUnitTest
JUnit 5. Essa extensão permite a realização de testes no estilo Arquillian que testam funcionalidades específicas. Ela não se destina a testar aplicações de usuário, pois isso deve ser feito por meio de io.quarkus.test.junit.QuarkusTest
. A principal diferença é que QuarkusTest
simplesmente inicializa o aplicativo uma vez no início da execução, enquanto QuarkusUnitTest
implanta uma aplicação Quarkus personalizada para cada classe de teste.
Esses testes devem ser colocados no módulo de implantação; se módulos adicionais do Quarkus forem necessários para o teste, seus módulos de implantação também deverão ser adicionados como dependências com escopo de teste.
Note que QuarkusUnitTest
está no módulo quarkus-junit5-internal
.
Um exemplo de classe de teste pode ter o seguinte aspecto:
package io.quarkus.health.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import io.quarkus.test.QuarkusUnitTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.restassured.RestAssured;
public class FailingUnitTest {
@RegisterExtension (1)
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() ->
ShrinkWrap.create(JavaArchive.class) (2)
.addClasses(FailingHealthCheck.class)
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
);
@Inject (3)
@Liveness
Instance<HealthCheck> checks;
@Test
public void testHealthServlet() {
RestAssured.when().get("/q/health").then().statusCode(503); (4)
}
@Test
public void testHealthBeans() {
List<HealthCheck> check = new ArrayList<>(); (5)
for (HealthCheck i : checks) {
check.add(i);
}
assertEquals(1, check.size());
assertEquals(HealthCheckResponse.State.DOWN, check.get(0).call().getState());
}
}
1 | A extensão QuarkusUnitTest deve ser usada com um campo estático. Se for utilizada com um campo não estático, a aplicação de teste não é iniciada. |
2 | Este produtor é usado para construir a aplicação a ser testada. Ele usa o Shrinkwrap para criar um JavaArchive para testar |
3 | É possível injetar beans da nossa implantação de teste diretamente no caso de teste |
4 | Este método invoca diretamente o Servlet de verificação de integridade e verifica a resposta |
5 | Este método usa o bean de verificação de integridade injetado para verificar se está devolvendo o resultado esperado |
Se você pretende testar se uma extensão falha corretamente em tempo de construção, use o método setExpectedException
:
package io.quarkus.hibernate.orm;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
public class PersistenceAndQuarkusConfigTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.setExpectedException(ConfigurationException.class) (1)
.withApplicationRoot((jar) -> jar
.addAsManifestResource("META-INF/some-persistence.xml", "persistence.xml")
.addAsResource("application.properties"));
@Test
public void testPersistenceAndConfigTest() {
// should not be called, deployment exception should happen first:
// it's illegal to have Hibernate configuration properties in both the
// application.properties and in the persistence.xml
Assertions.fail();
}
}
1 | Isto diz ao JUnit que a implantação do Quarkus deve falhar com uma exceção específica |
2.15. Testando recarga em tempo real
Também é possível escrever testes que verifiquem se uma extensão funciona corretamente no modo de desenvolvimento e se consegue lidar corretamente com as atualizações.
Para a maioria das extensões, isso funcionará sem ajustes, mas ainda assim é uma boa ideia fazer um teste de fumaça para verificar se essa funcionalidade está funcionando conforme o esperado. Para testar isso, usamos o site QuarkusDevModeTest
:
public class ServletChangeTestCase {
@RegisterExtension
final static QuarkusDevModeTest test = new QuarkusDevModeTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class) (1)
.addClass(DevServlet.class)
.addAsManifestResource(new StringAsset("Hello Resource"), "resources/file.txt");
}
});
@Test
public void testServletChange() throws InterruptedException {
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello World"));
test.modifySourceFile("DevServlet.java", new Function<String, String>() { (2)
@Override
public String apply(String s) {
return s.replace("Hello World", "Hello Quarkus");
}
});
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello Quarkus"));
}
@Test
public void testAddServlet() throws InterruptedException {
RestAssured.when().get("/new").then()
.statusCode(404);
test.addSourceFile(NewServlet.class); (3)
RestAssured.when().get("/new").then()
.statusCode(200)
.body(is("A new Servlet"));
}
@Test
public void testResourceChange() throws InterruptedException {
RestAssured.when().get("/file.txt").then()
.statusCode(200)
.body(is("Hello Resource"));
test.modifyResourceFile("META-INF/resources/file.txt", new Function<String, String>() { (4)
@Override
public String apply(String s) {
return "A new resource";
}
});
RestAssured.when().get("file.txt").then()
.statusCode(200)
.body(is("A new resource"));
}
@Test
public void testAddResource() throws InterruptedException {
RestAssured.when().get("/new.txt").then()
.statusCode(404);
test.addResourceFile("META-INF/resources/new.txt", "New File"); (5)
RestAssured.when().get("/new.txt").then()
.statusCode(200)
.body(is("New File"));
}
}
1 | Isso inicia a implantação, e seu teste pode modificá-la como parte do conjunto de testes. O Quarkus será reiniciado entre cada método de teste para que cada método comece com uma implantação limpa. |
2 | Esse método permite que você modifique o código-fonte de um arquivo de classe. O código-fonte antigo é passado para a função e o código-fonte atualizado é retornado. |
3 | Esse método adiciona um novo arquivo de classe à implantação. A fonte usada será a fonte original que faz parte do projeto atual. |
4 | Este método modifica um recurso estático |
5 | Este método adiciona um novo recurso estático |
2.16. Suporte a Executáveis Nativos
O Quarkus fornece vários itens de construção que controlam aspectos da construção do executável nativo. Isso permite que as extensões executem tarefas de forma programática, como o registro de classes para reflexão ou a adição de recursos estáticos ao executável nativo. Alguns desses itens de construção estão listados abaixo:
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem
-
Inclui recursos estáticos no executável nativo.
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem
-
Inclui os recursos estáticos do diretório no executável nativo.
io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem
-
Uma classe que será reinicializada em tempo de execução pelo Substrate. Isso fará com que o inicializador estático seja executado duas vezes.
io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem
-
Uma propriedade do sistema que será definida em tempo de construção do executável nativo.
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem
-
Inclui um pacote de recursos no executável nativo.
io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem
-
Registra uma classe para reflexão no Substrate. Os construtores são sempre registrados, enquanto os métodos e campos são opcionais.
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem
-
Uma classe que será inicializada em tempo de execução e não em tempo de construção. Isso fará com que a construção falhe se a classe for inicializada como parte do processo de construção do executável nativo, portanto, é preciso ter cuidado.
io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem
-
Uma funcionalidade de conveniência que permite controlar a maioria das funcionalidades acima referidas a partir de um único item de construção.
io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem
-
Indica que todos os conjuntos de caracteres devem ser ativados na imagem nativa.
io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem
-
Uma maneira conveniente de informar ao Quarkus que a extensão requer SSL e deve ser ativada durante a criação da imagem nativa. Ao usar esse recurso, lembre-se de adicionar sua extensão à lista de extensões que oferecem suporte a SSL automaticamente na guia nativo e ssl.
2.17. Sugestões de suporte à IDE
2.17.1. Escrevendo extensões Quarkus no Eclipse
O único aspecto particular da escrita de extensões Quarkus no Eclipse é o fato de a APT (Annotation Processing Tool - Ferramenta de Processamento de Anotação) ser necessária como parte das compilações de extensões, o que significa que é necessário:
-
Instalar
m2e-apt
a partir de https://marketplace.eclipse.org/content/m2e-apt -
Defina esta propriedade em
pom.xml
:<m2e.apt.activation>jdt_apt</m2e.apt.activation>
, no entando se você recorrer aio.quarkus:quarkus-build-parent
a obterá gratuitamente. -
Se tiver o projeto
io.quarkus:quarkus-extension-processor
aberto ao mesmo tempo no seu IDE (por exemplo, se tiver as fontes Quarkus verificadas e abertas no seu IDE), terá de fechar esse projeto. Caso contrário, o Eclipse não invocará o plug-in APT que ele contém. -
Se acabou de fechar o projeto do processador de extensões, certifique-se de fazer
Maven > Update Project
nos outros projetos para que o Eclipse vá buscar o processador de extensões no repositório Maven.
2.18. Dicas de Resolução de Problemas / Depuração
2.18.1. Inspecionando as Classes Geradas/Transformadas
O Quarkus gera muitas classes durante a fase de construção e, em muitos casos, também transforma as classes existentes. Muitas vezes, é extremamente útil ver o bytecode gerado e as classes transformadas durante o desenvolvimento de uma extensão.
If you set the quarkus.package.jar.decompiler.enabled
property to true
then Quarkus will download and invoke the Vineflower decompiler and dump the result in the decompiled
directory of the build tool output (target/decompiled
for Maven for example).
The output directory can be changed with quarkus.package.jar.decompiler.output-dir
.
Esta propriedade só funciona durante uma construção de produção normal (ou seja, não para o modo de desenvolvimento/testes) e quando o tipo de empacotamento fast-jar é usado (o comportamento padrão).
|
Existem também três propriedades do sistema que lhe permitem despejar as classes geradas/transformadas para o sistema de arquivos e inspecioná-las mais tarde, por exemplo, através de um descompilador no seu IDE.
-
quarkus.debug.generated-classes-dir
- para despejar as classes geradas, tais como metadados de beans -
quarkus.debug.transformed-classes-dir
- para despejar as classes transformadas, por exemplo, entidades Panache -
quarkus.debug.generated-sources-dir
- para despejar os arquivos ZIG; o arquivo ZIG é uma representação textual do código gerado que é referenciado nos rastreamentos de pilha
Estas propriedades são especialmente úteis no modo de desenvolvimento ou durante a execução dos testes em que as classes geradas/transformadas são apenas mantidas em memória num carregador de classes.
Por exemplo, você pode especificar a propriedade de sistema quarkus.debug.generated-classes-dir
para que estas classes sejam gravadas no disco para inspeção no modo de desenvolvimento:
./mvnw quarkus:dev -Dquarkus.debug.generated-classes-dir=dump-classes
O valor da propriedade pode ser um caminho absoluto, como /home/foo/dump numa máquina Linux, ou um caminho relativo ao diretório de trabalho do usuário, ou seja, dump corresponde a {user.dir}/target/dump no modo de desenvolvimento e {user.dir}/dump quando se executam os testes.
|
Você deve ver uma linha no registro para cada classe escrita no diretório:
INFO [io.qua.run.boo.StartupActionImpl] (main) Wrote /path/to/my/app/target/dump-classes/io/quarkus/arc/impl/ActivateRequestContextInterceptor_Bean.class
A propriedade também é honrada durante a execução de testes:
./mvnw clean test -Dquarkus.debug.generated-classes-dir=target/dump-generated-classes
De forma análoga, você pode usar as propriedades quarkus.debug.transformed-classes-dir
e quarkus.debug.generated-sources-dir
para despejar a saída relevante.
2.18.2. Inspecting Generated/Transformed Classes in QuarkusUnitTest
When using QuarkusUnitTest
,
as an alternative to setting quarkus.debug.*-dir
manually,
you may simply call QuarkusUnitTest#debugBytecode
:
public class MyTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.debugBytecode(true);
// ... test methods go here ...
}
This will automatically set up these configuration properties so that classes/sources
are dumped to target/debug
, for that test class only,
in a subdirectory that is unique to each test execution.
See the javadoc of QuarkusUnitTest#debugBytecode
for details.
This is handy to debug flaky tests that happen only in the CI environment, in particular;
for example the GitHub Actions CI at https://github.com/quarkusio/quarkus/
is set up so that such target/debug
directories are
collected into build artifacts available for download after each CI run.
2.18.3. Enabling trace logs for a particular test only
When using QuarkusUnitTest
,
if you need to enable trace logs for a particular test class,
you may simply call QuarkusUnitTest#traceCategories
and pass the logging categories in argument:
public class MyTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.traceCategories("org.hibernate", "io.quarkus.hibernate", "io.quarkus.panache");
// ... test methods go here ...
}
See the javadoc of QuarkusUnitTest#traceCategories
for details.
This is handy to debug flaky tests that happen only in the CI environment, in particular, as this will only increase the verbosity of logs in the particular test where the option is enabled.
2.18.4. Projetos Maven de Múltiplos Módulos e o Modo de Desenvolvimento
Não é incomum desenvolver uma extensão em um projeto Maven de múltiplos módulos que também contém um módulo de "exemplo". No entanto, se você quiser executar o exemplo no modo de desenvolvimento, a propriedade do sistema -DnoDeps
deverá ser usada para excluir as dependências do projeto local. Caso contrário, o Quarkus tentará monitorar as classes de extensão e isso pode resultar em problemas estranhos de carregamento de classes.
./mvnw compile quarkus:dev -DnoDeps
2.19. Exemplo de Extensão de Teste
Temos uma extensão que é usada para testar regressões no processamento da extensão. Ela está localizada no diretório https://github.com/quarkusio/quarkus/tree/main/integration-tests/test-extension/extension. Nesta seção, abordaremos algumas tarefas que um autor de extensão normalmente precisará executar usando o código test-extension para ilustrar como a tarefa pode ser realizada.
2.19.1. Funcionalidades e Capacidades
2.19.1.1. Características
Uma funcionalidade representa uma funcionalidade fornecida por uma extensão. O nome da funcionalidade é exibido no registro durante o bootstrap da aplicação.
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Quarkus 999-SNAPSHOT started in 0.061s.
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Installed features: [cdi, test-extension] (1)
1 | Uma lista de funcionalidades instaladas na imagem de tempo de execução |
A feature can be registered in a Processadores de Etapas de Construção method that produces a FeatureBuildItem
:
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem("test-extension");
}
O nome da funcionalidade deve conter apenas caracteres minúsculos, as palavras são separadas por traço; por exemplo, security-jpa
. Uma extensão deve fornecer no máximo uma funcionalidade e o nome deve ser exclusivo. Se várias extensões registrarem uma funcionalidade com o mesmo nome, a construção falhará.
The feature name should also map to a label in the extension’s devtools/common/src/main/filtered/extensions.json
entry so that
the feature name displayed by the startup line matches a label that one can use to select the extension when creating a project
using the Quarkus maven plugin as shown in this example taken from the Writing JSON REST Services guide where the rest-jackson
feature is referenced:
mvn io.quarkus.platform:quarkus-maven-plugin:3.16.3:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=rest-json \
-DclassName="org.acme.rest.json.FruitResource" \
-Dpath="/fruits" \
-Dextensions="rest,rest-jackson"
cd rest-json
2.19.1.2. Capacidades
Uma capacidade representa um recurso técnico que pode ser consultado por outras extensões. Uma extensão pode fornecer várias capacidades e várias extensões podem fornecer a mesma capacidade. Por padrão, as capacidades não são exibidas aos usuários. As capacidades devem ser usadas ao verificar a presença de uma extensão em vez de verificações baseadas em caminhos de classe.
Capabilities can be registered in a Processadores de Etapas de Construção method that produces a CapabilityBuildItem
:
@BuildStep
void capabilities(BuildProducer<CapabilityBuildItem> capabilityProducer) {
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-transactions"));
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-metrics"));
}
As extensões podem consumir capacidades registradas usando o item de construção Capabilities
:
@BuildStep
void doSomeCoolStuff(Capabilities capabilities) {
if (capabilities.isPresent(Capability.TRANSACTIONS)) {
// do something only if JTA transactions are in...
}
}
As capacidades devem seguir as convenções de nomenclatura dos pacotes Java; por exemplo, io.quarkus.security.jpa
. As capacidades fornecidas pelas extensões principais devem ser listados no enum io.quarkus.deployment.Capability
e seu nome deve sempre começar com o prefixo io.quarkus
.
2.19.2. Anotações de Definição de Bean
The CDI layer processes CDI beans that are either explicitly registered or that it discovers based on bean defining annotations as defined in 2.5.1. Bean defining annotations. You can expand this set of annotations to include annotations your extension processes using a BeanDefiningAnnotationBuildItem
as shown in this TestProcessor#registerBeanDefinningAnnotations
example:
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.jandex.DotName;
import io.quarkus.extest.runtime.TestAnnotation;
public final class TestProcessor {
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
static DotName TEST_ANNOTATION_SCOPE = DotName.createSimple(ApplicationScoped.class.getName());
...
@BuildStep
BeanDefiningAnnotationBuildItem registerX() {
(1)
return new BeanDefiningAnnotationBuildItem(TEST_ANNOTATION, TEST_ANNOTATION_SCOPE);
}
...
}
/**
* Marker annotation for test configuration target beans
*/
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Inherited
public @interface TestAnnotation {
}
/**
* A sample bean
*/
@TestAnnotation (2)
public class ConfiguredBean implements IConfigConsumer {
...
1 | Registre a classe de anotação e o escopo padrão CDI usando a classe Jandex DotName . |
2 | ConfiguredBean será processado pela camada CDI da mesma forma que um bean anotado com a @ApplicationScoped padrão CDI. |
2.19.3. Analisando Configurações para Objetos
Uma das principais coisas que uma extensão provavelmente fará é separar completamente a fase de configuração do comportamento da fase de tempo de execução. Os frameworks geralmente fazem a análise/carregamento da configuração na inicialização que pode ser feita durante o tempo de construção para reduzir as dependências de tempo de execução em frameworks como analisadores xml bem como reduzir o tempo de inicialização que a análise incorre.
Um exemplo de análise de um arquivo de configuração XML usando JAXB é mostrado no método TestProcessor#parseServiceXmlConfig
:
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml"); (1)
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is); (2)
...
}
return serviceBuildItem;
}
1 | Procura por um recurso classpath config.xml |
2 | Se for encontrado, analisa utilizando o contexto JAXB para XmlConfig.class |
Se não houvesse nenhum recurso /config.xml disponível no ambiente de construção, seria devolvido um |
Typically, one is loading a configuration to create some runtime component/service as parseServiceXmlConfig
is doing. We will come back to the rest of the behavior in parseServiceXmlConfig
in the following Gerenciar Serviço Não CDI section.
Se, por algum motivo, for necessário analisar a configuração e utilizá-la em outros passos de construção num processador de extensão, será necessário criar um XmlConfigBuildItem
para transmitir a instância XmlConfig analisada.
If you look at the XmlConfig code you will see that it does carry around the JAXB annotations. If you don’t want these in the runtime image, you could clone the XmlConfig instance into some POJO object graph and then replace XmlConfig with the POJO class. We will do this in Substituindo Classes na Imagem Nativa. |
2.19.4. Verificando Implantações Usando Jandex
Se a sua extensão definir anotações ou interfaces que marcam os beans que precisam ser processados, você pode localizar esses beans utilizando a API Jandex, um indexador de anotações Java e uma biblioteca de reflexão offline. O método TestProcessor#scanForBeans
a seguir mostra como localizar os beans anotados com nosso @TestAnnotation
que também implementam a interface IConfigConsumer
:
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
...
@BuildStep
@Record(STATIC_INIT)
void scanForBeans(TestRecorder recorder, BeanArchiveIndexBuildItem beanArchiveIndex, (1)
BuildProducer<TestBeanBuildItem> testBeanProducer) {
IndexView indexView = beanArchiveIndex.getIndex(); (2)
Collection<AnnotationInstance> testBeans = indexView.getAnnotations(TEST_ANNOTATION); (3)
for (AnnotationInstance ann : testBeans) {
ClassInfo beanClassInfo = ann.target().asClass();
try {
boolean isConfigConsumer = beanClassInfo.interfaceNames()
.stream()
.anyMatch(dotName -> dotName.equals(DotName.createSimple(IConfigConsumer.class.getName()))); (4)
if (isConfigConsumer) {
Class<IConfigConsumer> beanClass = (Class<IConfigConsumer>) Class.forName(beanClassInfo.name().toString(), false, Thread.currentThread().getContextClassLoader());
testBeanProducer.produce(new TestBeanBuildItem(beanClass)); (5)
log.infof("Configured bean: %s", beanClass);
}
} catch (ClassNotFoundException e) {
log.warn("Failed to load bean class", e);
}
}
}
1 | Depende de um BeanArchiveIndexBuildItem para que o passo de construção seja executado depois da implantação ter sido indexada. |
2 | Recupera o índice. |
3 | Encontra todos os beans anotados com @TestAnnotation . |
4 | Determina qual destes beans também tem a interface IConfigConsumer . |
5 | Salva a classe do bean em TestBeanBuildItem para utilizar num passo de construção RUNTIME_INIT posterior que irá interagir com as instâncias do bean. |
2.19.5. Interagindo Com os Beans de Extensão
Você pode utilizar a interface io.quarkus.arc.runtime.BeanContainer
para interagir com os seus beans de extensão. Os seguintes métodos configureBeans
ilustram a interação com os beans analisados na seção anterior:
// TestProcessor#configureBeans
@BuildStep
@Record(RUNTIME_INIT)
void configureBeans(TestRecorder recorder, List<TestBeanBuildItem> testBeans, (1)
BeanContainerBuildItem beanContainer, (2)
TestRunTimeConfig runTimeConfig) {
for (TestBeanBuildItem testBeanBuildItem : testBeans) {
Class<IConfigConsumer> beanClass = testBeanBuildItem.getConfigConsumer();
recorder.configureBeans(beanContainer.getValue(), beanClass, buildAndRunTimeConfig, runTimeConfig); (3)
}
}
// TestRecorder#configureBeans
public void configureBeans(BeanContainer beanContainer, Class<IConfigConsumer> beanClass,
TestBuildAndRunTimeConfig buildTimeConfig,
TestRunTimeConfig runTimeConfig) {
log.info("Begin BeanContainerListener callback\n");
IConfigConsumer instance = beanContainer.beanInstance(beanClass); (4)
instance.loadConfig(buildTimeConfig, runTimeConfig); (5)
log.infof("configureBeans, instance=%s\n", instance);
}
1 | Consome o TestBeanBuildItem produzido a partir da etapa de construção de varredura. |
2 | Consume o BeanContainerBuildItem para ordenar que este passo de construção seja executado depois que o contêiner de beans CDI tenha sido criado. |
3 | Chame o gravador de tempo de execução para registrar as interações do bean. |
4 | O gravador de tempo de execução recupera o bean usando o seu tipo. |
5 | O gravador de tempo de execução invoca o método IConfigConsumer#loadConfig(…) , passando os objetos de configuração com informações de tempo de execução. |
2.19.6. Gerenciar Serviço Não CDI
A common purpose for an extension is to integrate a non-CDI aware service into the CDI based Quarkus runtime.
Step 1 of this task is to load any configuration needed in a STATIC_INIT build step as we did in Analisando Configurações para Objetos.
Now we need to create an instance of the service using the configuration.
Let’s return to the TestProcessor#parseServiceXmlConfig
method to see how this can be done.
// TestProcessor#parseServiceXmlConfig
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml");
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is);
log.info("Loaded XmlConfig, creating service");
RuntimeValue<RuntimeXmlConfigService> service = recorder.initRuntimeService(config); (1)
serviceBuildItem = new RuntimeServiceBuildItem(service); (3)
}
return serviceBuildItem;
}
// TestRecorder#initRuntimeService
public RuntimeValue<RuntimeXmlConfigService> initRuntimeService(XmlConfig config) {
RuntimeXmlConfigService service = new RuntimeXmlConfigService(config); (2)
return new RuntimeValue<>(service);
}
// RuntimeServiceBuildItem
final public class RuntimeServiceBuildItem extends SimpleBuildItem {
private RuntimeValue<RuntimeXmlConfigService> service;
public RuntimeServiceBuildItem(RuntimeValue<RuntimeXmlConfigService> service) {
this.service = service;
}
public RuntimeValue<RuntimeXmlConfigService> getService() {
return service;
}
}
1 | Chame gravador de tempo de execução para registrar a criação do serviço. |
2 | Usando a instância analisada de XmlConfig , crie uma instância de RuntimeXmlConfigService e envolva-a numa RuntimeValue . Use um invólucro RuntimeValue para objetos que não sejam de interface e que são não-proxyable. |
3 | Envolva o valor do serviço de retorno num RuntimeServiceBuildItem para utilização num passo de construção RUNTIME_INIT que iniciará o serviço. |
2.19.6.1. Iniciando um Serviço
Agora que você gravou a criação de um serviço durante a fase de construção, precisa gravar como iniciar o serviço em tempo de execução durante a inicialização. Você faz isso com uma etapa de construção RUNTIME_INIT, conforme mostrado no método TestProcessor#startRuntimeService
.
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem , (1)
RuntimeServiceBuildItem serviceBuildItem) throws IOException { (2)
if (serviceBuildItem != null) {
log.info("Registering service start");
recorder.startRuntimeService(shutdownContextBuildItem, serviceBuildItem.getService()); (3)
} else {
log.info("No RuntimeServiceBuildItem seen, check config.xml");
}
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (4)
}
// TestRecorder#startRuntimeService
public void startRuntimeService(ShutdownContext shutdownContext, RuntimeValue<RuntimeXmlConfigService> runtimeValue)
throws IOException {
RuntimeXmlConfigService service = runtimeValue.getValue();
service.startService(); (5)
shutdownContext.addShutdownTask(service::stopService); (6)
}
1 | Consumimos um ShutdownContextBuildItem para registrar o encerramento do serviço. |
2 | Consumimos o serviço previamente inicializado capturado em RuntimeServiceBuildItem . |
3 | Chame o gravador de tempo de execução para gravar a invocação do início do serviço. |
4 | Produce a ServiceStartBuildItem to indicate the startup of a service. See Eventos de Inicialização e Encerramento for details. |
5 | O gravador de tempo de execução recupera a referência da instância do serviço e chama o seu método startService . |
6 | O registrador de tempo de execução registra uma invocação do método stopService da instância do serviço com o Quarkus ShutdownContext . |
O código para o RuntimeXmlConfigService
pode ser visualizado aqui: RuntimeXmlConfigService.java
O caso de teste para validar que o RuntimeXmlConfigService
foi iniciado pode ser encontrado no teste testRuntimeXmlConfigService
de ConfiguredBeanTest
e NativeImageIT
.
2.19.7. Eventos de Inicialização e Encerramento
O contêiner Quarkus suporta eventos de ciclo de vida de inicialização e encerramento para notificar os componentes sobre a inicialização e o encerramento do contêiner. Há eventos CDI disparados que os componentes podem observar, conforme ilustrado neste exemplo:
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
public class SomeBean {
/**
* Called when the runtime has started
* @param event
*/
void onStart(@Observes StartupEvent event) { (1)
System.out.printf("onStart, event=%s%n", event);
}
/**
* Called when the runtime is shutting down
* @param event
*/
void onStop(@Observes ShutdownEvent event) { (2)
System.out.printf("onStop, event=%s%n", event);
}
}
1 | Observe um StartupEvent para ser notificado de que o tempo de execução foi iniciado. |
2 | Observe um ShutdownEvent para ser notificado quando o tempo de execução for encerrado. |
What is the relevance of startup and shutdown events for extension authors? We have already seen the use of a ShutdownContext
to register a callback to perform shutdown tasks in the Iniciando um Serviço section.
These shutdown tasks would be called
after a ShutdownEvent
had been sent.
Um StartupEvent
é disparado após todos os produtores de io.quarkus.deployment.builditem.ServiceStartBuildItem
terem sido consumidos. A implicação disso é que, se uma extensão tiver serviços que os componentes da aplicação esperam que tenham sido iniciados quando observam um StartupEvent
, as etapas de construção que invocam o código de tempo de execução para iniciar esses serviços precisam produzir um ServiceStartBuildItem
para garantir que o código de tempo de execução seja executado antes que o StartupEvent
seja enviado. Lembre-se de que vimos a produção de um ServiceStartBuildItem
na seção anterior, e ela é repetida aqui para maior clareza:
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
RuntimeServiceBuildItem serviceBuildItem) throws IOException {
...
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (1)
}
1 | Produza um ServiceStartBuildItem para indicar que se trata de uma etapa de inicialização do serviço que tem de ser executada antes de o StartupEvent ser enviado. |
2.19.8. Registre Recursos para Uso na Imagem Nativa
Nem todas as configurações ou recursos podem ser consumidos no momento da construção. Se você tiver recursos do classpath que o tempo de execução precisa acessar, é necessário informar à fase de construção que esses recursos precisam ser copiados para a imagem nativa. Isso é feito produzindo um ou mais NativeImageResourceBuildItem
ou NativeImageResourceBundleBuildItem
no caso de pacotes de recursos. Exemplos disso são mostrados neste exemplo de etapa de construção registerNativeImageResources
:
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource, BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle) {
resource.produce(new NativeImageResourceBuildItem("/security/runtime.keys")); (1)
resource.produce(new NativeImageResourceBuildItem(
"META-INF/my-descriptor.xml")); (2)
resourceBundle.produce(new NativeImageResourceBuildItem("jakarta.xml.bind.Messages")); (3)
}
}
1 | Indica que o recurso de classpath /security/runtime.keys deve ser copiado para a imagem nativa. |
2 | Indica que o recurso META-INF/my-descriptor.xml deve ser copiado para a imagem nativa |
3 | Indica que o pacote de recursos "jakarta.xml.bind.Messages" deve ser copiado para a imagem nativa. |
2.19.9. Arquivos de serviço
Se você estiver usando arquivos META-INF/services
, será necessário registrar os arquivos como recursos para que a imagem nativa possa encontrá-los, mas também será necessário registrar cada classe listada para reflexão, para que possam ser instanciadas ou inspecionadas em tempo de execução:
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<ServiceProviderBuildItem> services) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// find out all the implementation classes listed in the service files
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
// register every listed implementation class so they can be instantiated
// in native-image at run-time
services.produce(
new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(),
implementations.toArray(new String[0])));
}
}
ServiceProviderBuildItem recebe uma lista de classes de implementação de serviço como parâmetros: se você não as estiver lendo do arquivo de serviço, certifique-se de que elas correspondam ao conteúdo do arquivo de serviço, pois o arquivo de serviço ainda será lido e usado em tempo de execução. Isso não substitui a gravação de um arquivo de serviço.
|
Isso apenas registra as classes de implementação para instanciação por meio de reflexão (você não poderá inspecionar seus campos e métodos). Se você precisar fazer isso, pode fazê-lo dessa forma: |
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectionClasses) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// register the service file so it is visible in native-image
resource.produce(new NativeImageResourceBuildItem(service));
// register every listed implementation class so they can be inspected/instantiated
// in native-image at run-time
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
reflectionClasses.produce(
new ReflectiveClassBuildItem(true, true, implementations.toArray(new String[0])));
}
}
Embora essa seja a maneira mais fácil de fazer com que seus serviços sejam executados nativamente, é menos eficiente do que verificar as classes de implementação em tempo de construção e gerar código que as registre no momento da inicialização estática, em vez de depender da reflexão.
Você pode conseguir isso adaptando a etapa de construção anterior para usar um gravador de inicialização estática em vez de registrar classes para reflexão:
public final class MyExtProcessor {
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerNativeImageResources(RecorderContext recorderContext,
SomeServiceRecorder recorder) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// read the implementation classes
Collection<Class<? extends io.quarkus.SomeService>> implementationClasses = new LinkedHashSet<>();
Set<String> implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
for(String implementation : implementations) {
implementationClasses.add((Class<? extends io.quarkus.SomeService>)
recorderContext.classProxy(implementation));
}
// produce a static-initializer with those classes
recorder.configure(implementationClasses);
}
}
@Recorder
public class SomeServiceRecorder {
public void configure(List<Class<? extends io.quarkus.SomeService>> implementations) {
// configure our service statically
SomeServiceProvider serviceProvider = SomeServiceProvider.instance();
SomeServiceBuilder builder = serviceProvider.getSomeServiceBuilder();
List<io.quarkus.SomeService> services = new ArrayList<>(implementations.size());
// instantiate the service implementations
for (Class<? extends io.quarkus.SomeService> implementationClass : implementations) {
try {
services.add(implementationClass.getConstructor().newInstance());
} catch (Exception e) {
throw new IllegalArgumentException("Unable to instantiate service " + implementationClass, e);
}
}
// build our service
builder.withSomeServices(implementations.toArray(new io.quarkus.SomeService[0]));
ServiceManager serviceManager = builder.build();
// register it
serviceProvider.registerServiceManager(serviceManager, Thread.currentThread().getContextClassLoader());
}
}
2.19.10. Substituição de Objetos
Os objetos criados durante a fase de construção que são passados para o tempo de execução têm de ter um construtor padrão para que sejam criados e configurados na inicialização do tempo de execução a partir do estado do tempo de construção. Se um objeto não tiver um construtor padrão, verá um erro semelhante ao seguinte durante a geração dos artefatos ampliados:
[error]: Build step io.quarkus.deployment.steps.MainClassBuildStep#build threw an exception: java.lang.RuntimeException: Unable to serialize objects of type class sun.security.provider.DSAPublicKeyImpl to bytecode as it has no default constructor
at io.quarkus.builder.Execution.run(Execution.java:123)
at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:136)
at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:110)
at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:99)
... 36 more
Existe uma interface io.quarkus.runtime.ObjectSubstitution
que pode ser implementada para dizer ao Quarkus como lidar com essas classes. Um exemplo de implementação para a interface DSAPublicKey
é mostrado aqui:
package io.quarkus.extest.runtime.subst;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.logging.Logger;
import io.quarkus.runtime.ObjectSubstitution;
public class DSAPublicKeyObjectSubstitution implements ObjectSubstitution<DSAPublicKey, KeyProxy> {
private static final Logger log = Logger.getLogger("DSAPublicKeyObjectSubstitution");
@Override
public KeyProxy serialize(DSAPublicKey obj) { (1)
log.info("DSAPublicKeyObjectSubstitution.serialize");
byte[] encoded = obj.getEncoded();
KeyProxy proxy = new KeyProxy();
proxy.setContent(encoded);
return proxy;
}
@Override
public DSAPublicKey deserialize(KeyProxy obj) { (2)
log.info("DSAPublicKeyObjectSubstitution.deserialize");
byte[] encoded = obj.getContent();
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encoded);
DSAPublicKey dsaPublicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance("DSA");
dsaPublicKey = (DSAPublicKey) kf.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
return dsaPublicKey;
}
}
1 | O método serialize pega o objeto sem um construtor padrão e cria um KeyProxy que contém as informações necessárias para recriar o DSAPublicKey . |
2 | O método deserialize utiliza o KeyProxy para recriar o DSAPublicKey a partir da sua forma codificada, utilizando a fábrica de chaves. |
Uma extensão registra esta substituição produzindo um ObjectSubstitutionBuildItem
, como mostra este fragmento de TestProcessor#loadDSAPublicKey
:
@BuildStep
@Record(STATIC_INIT)
PublicKeyBuildItem loadDSAPublicKey(TestRecorder recorder,
BuildProducer<ObjectSubstitutionBuildItem> substitutions) throws IOException, GeneralSecurityException {
...
// Register how to serialize DSAPublicKey
ObjectSubstitutionBuildItem.Holder<DSAPublicKey, KeyProxy> holder = new ObjectSubstitutionBuildItem.Holder(
DSAPublicKey.class, KeyProxy.class, DSAPublicKeyObjectSubstitution.class);
ObjectSubstitutionBuildItem keysub = new ObjectSubstitutionBuildItem(holder);
substitutions.produce(keysub);
log.info("loadDSAPublicKey run");
return new PublicKeyBuildItem(publicKey);
}
2.19.11. Substituindo Classes na Imagem Nativa
O Graal SDK oferece suporte a substituições de classes na imagem nativa. Um exemplo de como é possível substituir as classes XmlConfig/XmlData
por versões que não têm dependências de anotação JAXB é mostrado nestas classes de exemplo:
package io.quarkus.extest.runtime.graal;
import java.util.Date;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.quarkus.extest.runtime.config.XmlData;
@TargetClass(XmlConfig.class)
@Substitute
public final class Target_XmlConfig {
@Substitute
private String address;
@Substitute
private int port;
@Substitute
private ArrayList<XData> dataList;
@Substitute
public String getAddress() {
return address;
}
@Substitute
public int getPort() {
return port;
}
@Substitute
public ArrayList<XData> getDataList() {
return dataList;
}
@Substitute
@Override
public String toString() {
return "Target_XmlConfig{" +
"address='" + address + '\'' +
", port=" + port +
", dataList=" + dataList +
'}';
}
}
@TargetClass(XmlData.class)
@Substitute
public final class Target_XmlData {
@Substitute
private String name;
@Substitute
private String model;
@Substitute
private Date date;
@Substitute
public String getName() {
return name;
}
@Substitute
public String getModel() {
return model;
}
@Substitute
public Date getDate() {
return date;
}
@Substitute
@Override
public String toString() {
return "Target_XmlData{" +
"name='" + name + '\'' +
", model='" + model + '\'' +
", date='" + date + '\'' +
'}';
}
}
3. Integração do ecossistema
Some extensions may be private, and some may wish to be part of the broader Quarkus ecosystem, and available for community re-use. Inclusion in the Quarkiverse Hub is a convenient mechanism for handling continuous testing and publication. The Quarkiverse Hub wiki has instructions for on-boarding your extension.
Alternativamente, os testes contínuos e a publicação podem ser efetuados manualmente.
3.1. Testes contínuos da sua extensão
Para facilitar que os autores de extensões testem suas extensões diariamente em relação ao último snapshot do Quarkus, o Quarkus introduziu a noção de Ecosystem CI. O README do Ecosystem CI tem todos os detalhes sobre como configurar um job do GitHub Actions para aproveitar esse recurso, enquanto este vídeo fornece uma visão geral de como é o processo.
3.2. Publique a sua extensão em registry.quarkus.io
Antes de publicar a sua extensão nas ferramentas Quarkus, certifique-se de que os seguintes requisitos são cumpridos:
-
O arquivo quarkus-extension.yaml (no módulo
runtime/
da extensão) tem o conjunto mínimo de metadados:-
name
-
description
(a menos que já esteja definido no elemento<description>
doruntime/pom.xml
, que é a abordagem recomendada)
-
-
A sua extensão é publicada no Maven Central
-
Your extension repository is configured to use the Ecosystem CI.
Em seguida, deve criar um pull request adicionando um arquivo your-extension.yaml
no diretório extensions/
no Catálogo de Extensões do Quarkus. O YAML deve ter a seguinte estrutura:
group-id: <YOUR_EXTENSION_RUNTIME_GROUP_ID>
artifact-id: <YOUR_EXTENSION_RUNTIME_ARTIFACT_ID>
Quando o repositório contém várias extensões, você precisa criar um arquivo separado para cada extensão individual, e não apenas um arquivo para todo o repositório. |
Isso é tudo. Assim que o pull request for integrado, um job agendado irá verificar o Maven Central para novas versões e atualizar o Registro de Extensões Quarkus.