Fornecendo Componentes Injetáveis

Published by

on

Ao escrever aplicações, a escolha de um mecanismo de injeção de dependências é direta. Podemos escolher qualquer mecanismo que for conveniente dentro das nossas próprias restrições, já que temos a arquitetura na mão. Trata-se de tomar a decisão, acrescentar as dependências adequadas ao projeto, anotar nossos fontes com as anotações de injeção e pronto.

Ao fornecer componentes para um universo amplo de aplicações, porém, não temos o mesmo luxo. Aplicações diferentes, escritas e mantidas por times diferentes, naturalmente terão arquiteturas diferentes. Não é adequado para um componente constranger a aplicação com a escolha desse mecanismo. Componentes devem manter uma ampla condição de reuso.

Neste artigo, vou demonstrar como fornecer um componente capaz de injeção por diferentes mecanismos, sem onerar a aplicação com dependências irrelevantes.

Antes de prosseguir, é importante situar a injeção de dependências como um mecanismo e uma técnica dentro do conceito maior de arquitetura de componentes. No contexto dessa discussão, trata-se de definir a arquitetura como uma montagem de partes intercambiáveis: partes cuja substituição não exige adaptação de nenhuma outra parte da arquitetura. No lingo da computação, definimos corretas interfaces para permitir variar a implementação sem maiores custos. Em particular, quando podemos trocar um módulo defeituoso por outro corrigido sem exigir a construção de fontes, estamos no caminho certo. Mecanismo de injeção participam desse processo facilitando consumir implementações desconhecidas, ou seja, tornando fácil consumir interfaces sem fazer referência para a implementação.

Estamos portanto sempre falando de prover uma implementação em algum tipo de ambiente para o consumidor da interface correspondente. Vamos supor uma interface br.dev.pedrolamarao.foo.FooService. Nosso objetivo é oferecer uma implementação de FooService para um consumidor protegido pelo mecanismo de injeção.

Anotações

Com o advento das anotações, diversas formas de programação orientada a aspectos se tornou viável, inclusive a fácil configuração de relações de dependência. Vamos olhar para dois mecanismos diferentes: containeres EJB e frameworks OSGi.

Assim nós podemos consumir uma implementação de FooService no container EJB:

@EJB
public void setFoo  (FooService dependency) { ... }

O container EJB usará setFoo para injetar uma implementação de FooService na etapa adequada do ciclo de vida do objeto. Assim nós podemos fornecer uma implementação de FooService no container EJB:

@Stateless
public class FooImpl implements FooService { ... }

O container EJB processa todas as classes e, diante de @Stateless, configura-se de modo a fornecer objetos do tipo FooImpl aos consumidores de FooService.

A mesma facilidade está disponível para frameworks OSGi. Aparte a configuração necessária para todo bundle, assim nós podemos consumir uma implementação de FooService no framework OSGi:

@Reference
public void bindFoo (FooService dependency) { ... }

O framework OSGi usará bindFoo para injetar uma implementação de FooService na etapa adequada do ciclo de vida do objeto. Assim nós podemos fornecer uma implementação de FooService no framework OSGi:

@Component
public class FooImpl implements FooService { ... }

O ferramental OSGi processa todas as classes e, diante de @Component, gera a configuração necessária para fornecer objetos do tipo FooImpl aos consumidores de FooService.

Com certeza, para prototipar, para experimentar, para escrever aplicações e em diversos outros cenários, usar anotações torna muito mais fácil a configuração e o uso de mecanismo de injeção, promovendo o seu uso. Agora, como nós poderíamos fornecer um componente capaz de participar tanto do container EJB quanto do framework OSGi? Talvez algo assim:

@Component
@Stateless
public class FooImpl implements FooService { ... }

Acredito que isto é fundamentalmente inviável. Talvez, caso a necessidade esteja fechada para somente EJB e OSGi, esta solução seja aceitável. Porém, caso hajam mais mecanismos a suportar, ou caso o conjunto de mecanismos a suportar no futuro seja aberto, esta solução não parece escalável. Qualquer seja nossa opinião sobre a aparência da declaração acima, resta que este componente tratá para a aplicação transitivamente dependências em todos esses mecanismos. A arquitetura da aplicação sofrerá o acréscimo de vários módulos de injeção irrelevantes.

Descritores

Antes do advento das anotações, componentes eram configurados por arquivo de configuração, às vezes chamados descritores. Lidar com esses artigos impõe um custo notório ao desenvolvimento: por exemplo, durante a fase de prototipação, em que tudo muda muito rápido, manter a paridade entre o código e a configuração é uma tarefa cansativa e desestimulante.

Mesmo assim, a configuração por arquivo tem uma virtude muito simples: ela não impõe exigência alguma ao compilador. Não é necessário acrescentar os módulos que definem anotações de qualquer tipo. É suficiente que a configuração esteja correta e presente no local certo.

Descritores permitem alcançar o objetivo de suportar múltiplos mecanismo de injeção, sem prejuízo de acrescentar novos mecanismos no futuro, sem sobrecarrega a aplicação com dependências transitivas. O descritor informa ao container ou framework fundamentalmente qual implementação fornece qual interface, com as propriedades e parâmetros que são adequados para o caso. Vamos demonstrar como preparar nossa implementação para suportar quatro mecanismos ao mesmo tempo: ServiceLoader, EJB, CDI e OSGi.

Adiantamos o resultado final, a listagem de arquivos que integram o componente.

/br/dev/pedrolamarao/foo/impl/FooImpl
/META-INF/MANIFEST.MF
/META-INF/services/br.dev.pedrolamarao.foo.FooService
/META-INF/ejb-jar.xml
/META-INF/beans.xml
/OSGI-INF/FooService.xml

Para fornece FooImpl para consumidores através do container EJB, basta incluir o arquivo /META-INF/ejb-jar.xml com o seguinte conteúdo:

<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd" version="3.1">
 <enterprise-beans>
  <session>
   <ejb-name>FooService</ejb-name>
   <ejb-class>br.dev.pedrolamarao.foo.impl.FooImpl</ejb-class>
   <session-type>Stateless</session-type>
  </session>
 </enterprise-beans>
</ejb-jar>

O container EJB aplicará a mesma configuração para o caso da anotação correspondente. Consumidores podem usar a anotação @EJB normalmente.

Para fornecer FooImpl para consumidores através do container CDI, vamos aproveitar a configuração para o container EJB com o acréscimo do arquivo /META-INF/beans.xml:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" bean-discovery-mode="annotated" version="2.0">
</beans>

Esta configuração funciona em conjunto com a anterior: o container CDI fornecerá FooImpl para consumidores como faria o container EJB. Consumidores podem usar a anotação @Inject normalmente.

Para fornecer FooImpl para consumidores através do framework OSGi, primeiro acrescentamos uma diretiva no /META-INF/MANIFEST.MF:

'Service-Component' : 'OSGI-INF/file.xml'

O framework OSGi consultará o MANIFEST.MF em busca de diretivas Service-Component. O arquivo indicado será tomado como a definição de um serviço injetável. O arquivo /OSGI-INF/FooService.xml tem o seguinte conteúdo:

<component xmlns="http://www.osgi.org/xmlns/scr/v1.4.0" name="prodist.robot.file" immediate="true">
 <implementation class="br.dev.pedrolamarao.foo.impl.FooImpl"/>
 <service>
  <provide interface="br.dev.pedrolamarao.foo.FooService"/>
 </service>
</component>

O framework OSGi aplicará a configuração de acordo. Consumidores podem usar a anotação @Reference normalmente.

Por fim, para fornecer FooImpl para consumidores de br.dev.pedrolamarao.foo.FooService através de ServiceLoader, basta incluir o arquivo /META-INF/services/br.dev.pedrolamarao.foo.FooService com o seguinte conteúdo:

br.dev.pedrolamarao.foo.impl.FooImpl

Quando o consumidor de FooService solicitar ao ServiceLoader, ele buscará em todos os módulos disponíveis um arquivo com o nome correspondente, e tomará seu conteúdo como uma listagem de nomes de implementação. Não existe anotação para ServiceLoader!

Desta maneira, sem nenhum impacto no grafo de dependências da aplicação, fornecemos um componente capaz de integração a quatro mecanismos diferente de injeção. Manter todos esses arquivos atualizados durante a prototipagem com certeza é uma tarefa muito cansativa e desestimulante. Porém, com o passar das etapas de desenvolvimento e o amadurecimento da implementação, este é com certeza um caminho muito vantajoso para permitir a máxima integrabilidade sem impacto negativo.

Para saber mais sobre ServiceLoader, veja o Javadoc da versão 8 aqui.

Para saber mais sobre EJB e CDI, eu acho interessante começar pelo JavaEE Tutorial aqui.

Para saber mais sobre serviços OSGi, sugiro começar por aqui.

Deixe um comentário

Previous Post
Next Post