Extensão portátil e passagem CDI

Extensão portátil e passagem CDI

1. Visão geral

Neste tutorial, examinaremos um recurso interessante do CDI (Context and Dependency Injection) chamado extensão portátil CDI.

Primeiro, começaremos entendendo como funciona e, em seguida, veremos como escrever uma extensão. Percorreremos as etapas para implementar um módulo de integração de CDI para Flyway, para que possamos executar uma migração de banco de dados na inicialização de um contêiner de CDI.

Este tutorial pressupõe um entendimento básico do CDI. Dê uma olhada emthis article para uma introdução ao CDI.

2. O que é uma extensão portátil CDI?

Uma extensão portátil CDI é um mecanismo pelo qual podemos implementar funcionalidades adicionais na parte superior do contêiner CDI. No momento da inicialização, o contêiner CDI varre o caminho de classe e cria metadados sobre as classes descobertas.

During this scanning process, the CDI container fires many initialization events which can be only observed by extensions. É aqui que uma extensão portátil CDI entra em jogo.

Uma extensão CDI portátil observa esses eventos e, em seguida, modifica ou adiciona informações aos metadados criados pelo contêiner.

3. Dependências do Maven

Vamos começar adicionando a dependência necessária para a API CDI empom.xml. É suficiente para implementar uma extensão vazia.


    javax.enterprise
    cdi-api
    2.0.SP1

E para executar o aplicativo, podemos usar qualquer implementação compatível com CDI. Neste artigo, usaremos a implementação do Weld.


    org.jboss.weld.se
    weld-se-core
    3.0.5.Final
    runtime

Você pode verificar se alguma nova versão dethe APIethe implementation foi lançada no Maven Central.

4. Executando Flyway em um ambiente não CDI

Antes de começarmos a integrarFlywaye CDI, devemos primeiro ver como executá-lo em um contexto não CDI.

Então, vamos dar uma olhada no seguinte exemplo tirado dehttps://flywaydb.org/getstarted/firststeps/api:

DataSource dataSource = //...
Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
flyway.migrate();

Como podemos ver, estamos usando apenas uma instânciaFlyway que precisa de uma instânciaDataSource.

Nossa extensão portátil CDI produzirá posteriormente os beansFlywayeDatasource. Para o propósito deste exemplo, usaremos um banco de dados H2 incorporado e forneceremos as propriedadesDataSource por meio da anotaçãoDataSourceDefinition.

5. Eventos de inicialização de contêiner CDI

Na inicialização do aplicativo, o contêiner CDI inicia carregando e instanciando todas as extensões portáteis CDI. Em seguida, em cada extensão, ele pesquisa e registra métodos observadores de eventos de inicialização, se houver. Depois disso, ele executa as seguintes etapas:

  1. Dispara o eventoBeforeBeanDiscovery antes do início do processo de digitalização

  2. Executa a descoberta de tipo em que verifica os beans de arquivo e para cada tipo descoberto dispara o eventoProcessAnnotatedType

  3. Dispara o eventoAfterTypeDiscovery

  4. Executa a descoberta de bean

  5. Dispara oAfterBeanDiscovery etenta

  6. Executa a validação de bean e detecta erros de definição

  7. Dispara o eventoAfterDeploymentValidation

A intenção de uma extensão portátil CDI é observar esses eventos, verificar metadados sobre os beans descobertos, modificar esses metadados ou adicioná-los.

Em uma extensão portátil CDI, podemos apenas observar esses eventos.

6. Gravando a extensão portátil CDI

Vamos ver como podemos nos conectar a alguns desses eventos, construindo nossa própria extensão portátil CDI.

6.1. Implementando o provedor SPI

Uma extensão portátil CDI é um provedor Java SPI da interfacejavax.enterprise.inject.spi.Extension. Dê uma olhada emthis article para uma introdução ao Java SPI.

Primeiro, começamos fornecendo a implementaçãoExtension. Posteriormente, adicionaremos métodos observadores aos eventos de inicialização do contêiner CDI:

public class FlywayExtension implements Extension {
}

Em seguida, adicionamos um nome de arquivoMETA-INF/services/javax.enterprise.inject.spi.Extension com este conteúdo:

com.example.cdi.extension.FlywayExtension

Como um SPI, esteExtension é carregado antes do bootstrap do contêiner. Portanto, os métodos observadores nos eventos de inicialização do CDI podem ser registrados.

6.2. Definindo Métodos Observadores de Eventos de Inicialização

Neste exemplo, tornamos a classeFlyway conhecida para o contêiner CDI antes do início do processo de digitalização. Isso é feito no método do observadorregisterFlywayType():

public void registerFlywayType(
  @Observes BeforeBeanDiscovery bbdEvent) {
    bbdEvent.addAnnotatedType(
      Flyway.class, Flyway.class.getName());
}

Aqui, adicionamos metadados sobre a classeFlyway. From now on, it’ll behave as if it was scanned by the container. Para este propósito, usamos o métodoaddAnnotatedType().

A seguir, observaremos o eventoProcessAnnotatedType para tornar a classeFlyway um bean gerenciado por CDI:

public void processAnnotatedType(@Observes ProcessAnnotatedType patEvent) {
    patEvent.configureAnnotatedType()
      .add(ApplicationScoped.Literal.INSTANCE)
      .add(new AnnotationLiteral() {})
      .filterMethods(annotatedMethod -> {
          return annotatedMethod.getParameters().size() == 1
            && annotatedMethod.getParameters().get(0).getBaseType()
              .equals(javax.sql.DataSource.class);
      }).findFirst().get().add(InjectLiteral.INSTANCE);
}

Primeiro, anotamos a classeFlyway com anotações@ApplicationScopede@FlywayType, então procuramos o métodoFlyway.setDataSource(DataSource dataSource)e anotamos por@Inject.

O resultado final das operações acima tem o mesmo efeito como se o contêiner varresse o seguinte beanFlyway:

@ApplicationScoped
@FlywayType
public class Flyway {

    //...
    @Inject
    public void setDataSource(DataSource dataSource) {
      //...
    }
}

A próxima etapa é disponibilizar um beanDataSource para injeção, pois nosso beanFlyway depende de um beanDataSource.

Para isso, iremos processar para registrar um BeanDataSource no contêiner e usaremos o eventoAfterBeanDiscovery:

void afterBeanDiscovery(@Observes AfterBeanDiscovery abdEvent, BeanManager bm) {
    abdEvent.addBean()
      .types(javax.sql.DataSource.class, DataSource.class)
      .qualifiers(new AnnotationLiteral() {}, new AnnotationLiteral() {})
      .scope(ApplicationScoped.class)
      .name(DataSource.class.getName())
      .beanClass(DataSource.class)
      .createWith(creationalContext -> {
          DataSource instance = new DataSource();
          instance.setUrl(dataSourceDefinition.url());
          instance.setDriverClassName(dataSourceDefinition.className());
              return instance;
      });
}

Como podemos ver, precisamos de umDataSourceDefinition que fornece as propriedades do DataSource.

Podemos anotar qualquer bean gerenciado com a seguinte anotação:

@DataSourceDefinition(
  name = "ds",
  className = "org.h2.Driver",
  url = "jdbc:h2:mem:testdb")

Para extrair essas propriedades, observamos o eventoProcessAnnotatedType junto com a anotação@WithAnnotations:

public void detectDataSourceDefinition(
  @Observes @WithAnnotations(DataSourceDefinition.class) ProcessAnnotatedType patEvent) {
    AnnotatedType at = patEvent.getAnnotatedType();
    dataSourceDefinition = at.getAnnotation(DataSourceDefinition.class);
}

E, finalmente, ouvimos o eventoAfterDeployementValidation para obter o beanFlyway desejado do contêiner CDI e, em seguida, invocamos o métodomigrate():

void runFlywayMigration(
  @Observes AfterDeploymentValidation adv,
  BeanManager manager) {
    Flyway flyway = manager.createInstance()
      .select(Flyway.class, new AnnotationLiteral() {}).get();
    flyway.migrate();
}

7. Conclusão

Construir uma extensão portátil CDI parece difícil na primeira vez, mas depois que entendemos o ciclo de vida de inicialização do contêiner e o SPI dedicado a extensões, ele se torna uma ferramenta muito poderosa que podemos usar para criar estruturas sobre o Java EE.

Como de costume, todos os exemplos de código mostrados neste artigo podem ser encontradosover on GitHub.