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

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.

Esta é uma documentação aprofundada, consulte a seção construindo minha primeira extensão se precisar de uma introdução.

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:

Classe de Bean Registrada por 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:

Bean Injetando um Bean Produzido por um 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:

Exemplo de Injeção de Bean de Extensão
@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.

Instância Produzida Através do Gravador
@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.
Portador do "Contexto"
@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:

  1. 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.

  2. 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:

  1. Um submódulo de tempo de implantação que trata do processamento do tempo de construção e da gravação de bytecode.

  2. 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) ou quarkus/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.15.1: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.15.1: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ório extensions/my-ext

    • quarkus-my-ext no diretório extensions/my-ext/runtime

    • quarkus-my-ext-deployment no diretório extensions/my-ext/deployment; neste módulo é gerada uma classe básica MyExtProcessor.

    • quarkus-my-ext-integration-test no diretório integration-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> do quarkus-extensions-parent

    • quarkus-my-ext é adicionado ao <dependencyManagement> do BOM (Bill of Materials) do Quarkus bom/application/pom.xml

    • quarkus-my-ext-deployment é adicionado ao <dependencyManagement> do BOM (Bill of Materials) do Quarkus bom/application/pom.xml

    • quarkus-my-ext-integration-test é adicionado ao sítio <modules> de quarkus-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.15.1')
}

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.

Exemplo de um item de construção individual
/**
 * 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.

Exemplo de um item de construção múltipla
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;
    }
}
Exemplo de utilização de vários itens de construção
/**
 * 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) .
Exemplo de um item de construção vazio
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.

Exemplo de utilização de um item de construção vazio num estilo "pseudo-alvo"
/**
 * 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)
    // ...
}
Exemplo de utilização de um item de construção vazio num estilo "barreira"
/**
 * 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.

Exemplo de utilização de um item de construção de erro de validação num estilo "pseudo-alvo"
@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.

Exemplo de passo de construção que é sempre executado num estilo "pseudo-alvo"
@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:

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.

Exemplo de produção de um item de construção de forma fraca
/**
 * 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.

Exemplo de utilização de um gravador de bytecode em que a classe gerada é fracamente produzida
/**
 * 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

BUILD_TIME

Adequado para coisas que afetam a construção.

BUILD_AND_RUN_TIME_FIXED

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.

RUN_TIME

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 campo file desta classe,

  • enable - um segmento de nome que corresponde ao campo enable em FileConfig.

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 ( -- ) não são compatíveis com a documentação do AsciiDoc. Todos os outros tipos de blocos (fonte, advertências…​) são compatíveis.

Por padrão, o gerador de documentação usará o nome do campo hifenizado como a chave de um java.util.Map . Use a anotação io.quarkus.runtime.annotations.ConfigDocMapKey para substituir o comportamento.

@ConfigMapping(prefix = "quarkus.some")
@ConfigRoot
public interface SomeConfig {
    /**
     * Namespace configuration.
     */
    @WithParentName
    @ConfigDocMapKey("cache-name") (1)
    Map<String, Name> namespace();
}
1 Isto irá gerar uma chave de mapa de configuração com o nome quarkus.some."cache-name" em vez de quarkus.some."namespace".

É possível escrever uma explicação textual para o valor padrão da documentação, o que é útil quando ela é gerada: @ConfigDocDefault("explain how this is generated").

@ConfigDocEnumValue oferece uma maneira de personalizar explicitamente o texto exibido na documentação ao listar os valores aceitos para um enum.

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 com leveloffset=+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.

Um exemplo de um passo de construção condicional
@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:

Condição em toda a classe para o passo de construção 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. 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 @IgnoreProperty pode ser colocado no campo.

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 io.quarkus.deployment.recording.RecordingAnnotationsProvider.

Este mesmo SPI também pode ser utilizado para fornecer uma anotação personalizada que substituirá @RecordableConstructor.

2.6.1. 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.2. 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. 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.6.4. 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.7. Contextos e Injeção de Dependência

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 um MetricsFactory. 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 classe Statistics 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 utilizando MetricsFactory::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:

  1. Uma biblioteca subjacente utilizada pela extensão está usando diretamente uma API de métricas específica (MP Metrics, Micrometer ou outra).

  2. 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.x MetricsOptions.

  3. 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étodo MetricsFactory::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 um MetricsFactoryConsumerBuildItem que usa um Gravador RUNTIME_INIT ou STATIC_INIT para definir um consumidor MetricsFactory. Por exemplo, o seguinte cria um MetricsFactoryConsumerBuildItem 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 API MetricFactory para observar os métodos io.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 a io.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).

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. 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.18.3. O indexador não inclui a sua dependência externa

Não se esqueça de adicionar os artefatos IndexDependencyBuildItem ao seu @BuildStep.

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.

Linhas de Inicialização de Exemplo
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:

TestProcessor#feature()
    @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.15.1: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:

TestProcessor#capability()
    @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:

TestProcessor#doSomeCoolStuff()
    @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:

Registrando uma Anotação de Definição de Bean
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 :

Analisando uma Configuração XML em uma Instância XmlConfig em Tempo de Execução
    @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 RuntimeServiceBuildItem nulo e não seria executada qualquer lógica subsequente baseada num RuntimeServiceBuildItem produzido.

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:

Exemplo de Uso do Jandex
    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:

Usando a Interface CDI BeanContainer
// 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.

Criando um Serviço Não CDI
// 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 .

Iniciando/Parando um Serviço Não CDI
// 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:

Observando a Inicialização do Contêiner
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:

Exemplo de Produção de um ServiceStartBuildItem
// 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:

Registrando Recursos e Pacotes de Recursos
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:

Erro de Serialização de DSAPublicKey
	[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:

Exemplo de DSAPublicKeyObjectSubstitution
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:

Registrando uma Substituição de Objeto
    @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:

Exemplo de Substituição das Classes XmlConfig/XmlData
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> do runtime/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.

Conteúdo Relacionado