Simplificando Service Providers com módulos do Java 9+

Num post anterior, vimos como usar Service Providers na implementação de um plugin de temas da empresa Paradizo para o Cotuba, uma aplicação que gera PDFs e EPUBs a partir de arquivos Markdown. Nesse post, veremos como simplificar a criação de Service Providers com a solução de modularização disponível Java 9 em diante.

Modularizando com o JPMS

A partir do Java 9, podemos definir módulos usando o JPMS (Java Platform Module System).

No arquivo module-info.java do Cotuba, vamos descrever o módulo cotuba:


module cotuba {
}

Um módulo permite a criação de encapsulamento um nível acima de pacotes ou classes. É possível escolher quais pacotes de um JAR serão expostos para outros módulos.

Vamos exportar os pacotes:
cotuba.plugin, que contém as interfaces LogoAposRenderizarMDParaHTML e AoFinalizarGeracao
cotuba.domain, que contém as classes Ebook e Capitulo, entre outras.

Para isso, devemos usar o exports:


module cotuba {
  exports cotuba.plugin;
  exports cotuba.domain;
}

Usamos o requires para definir de quais outros módulos dependemos:


module cotuba {
  exports cotuba.plugin;
  exports cotuba.domain;

  requires commons.cli;
  requires org.commonmark;
  requires epublib.core;
  requires html2pdf;
  //outras dependências...
}

No Cotuba, usamos as bibliotecas Commons CLI, CommonMark, Epublib e iText. Cada um desses tem suas próprias dependências.

A equipe da Paradizo também modularizou o tema:


module tema.paradizo {
  requires cotuba;
  requires jsoup;
}

O tema da Paradizo depende da biblioteca JSoup e do módulo do Cotuba.

No caso de dependermos de um JAR ainda não modularizado, é criado um módulo automaticamente, que expõe todos os pacotes.

O nome do módulo automático será definido pela configuração Automatic-Module-Name do arquivo META-INF/MANIFEST.MF, o que acontece na biblioteca CommonMark.

Caso esse arquivo não exista, o nome do próprio JAR será usado. É o caso para as demais bibliotecas.

Com o JPMS ainda podemos executar a aplicação com um Classpath, por motivos de retrocompatibilidade. Porém, não teríamos as vantagens da modularização.

Para executarmos de maneira modularizada, devemos usar a opção --module-path. Além disso, precisamos usar a opção --module para indicar qual é o módulo da classe Main:


java --module-path libs --module cotuba/cotuba.Main -f epub

O resultado do comando anterior é o seguinte erro:


Exception in thread "main" java.util.ServiceConfigurationError: 
  cotuba.plugin.LogoAposRenderizarMDParaHTML:
    module cotuba does not declare `uses`
  at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:588)
  ...
  at cotuba/cotuba.plugin.LogoAposRenderizarMDParaHTML.rodaPara(LogoAposRenderizarMDParaHTML.java:13)

Quando usamos módulos do JPMS, precisamos mudar um pouco a maneira de definir os service providers e interfaces.

Usando a ServiceLoader API em módulos do JPMS

Em módulos do JPMS, as Service Provider Interfaces (SPI) devem ser definidas com o uses.

No módulo cotuba, teremos:


module cotuba {
  exports cotuba.plugin;
  exports cotuba.domain;

  requires org.commonmark;
  requires epublib.core;
  requires html2pdf;
  //outras dependências...

  uses cotuba.plugin.LogoAposRenderizarMDParaHTML; //inserido
  uses cotuba.plugin.AoFinalizarGeracao; //inserido
}

Já no código do tema da Paradizo, indicamos um service provider com provides ... with:


module tema.paradizo {
  requires cotuba;
  requires jsoup;

  provides cotuba.plugin.LogoAposRenderizarMDParaHTML
      with br.com.paradizo.cotuba.tema.TemaParadizo; //inserido
}

Podemos excluir o arquivo META-INF/services/cotuba.plugin.LogoAposRenderizarMDParaHTML, já que um módulo do Java 9 ignora esse arquivo.

A definição de SPIs com uses e de service providers com provides ... with nos módulos do JPMS simplifica o uso da Service Loader API.

Executamos com o mesmo comando anterior:


java --module-path libs --module cotuba/cotuba.Main -f epub

O resultado será:


Executando o tema Paradizo para o capítulo: Aberturas de livros
Executando o tema Paradizo para o capítulo: Frases
Arquivo gerado com sucesso: book.epub

O EPUB foi gerado com sucesso e a borda do no título do capítulo foi aplicada.

Foto do EPUB gerado com o tema da Paradizo

O código pode ser encontrado em:
https://github.com/alexandreaquiles/cotuba/tree/java9

Plugins na sua arquitetura

Há um estilo arquitetural em que o domain model é o núcleo da aplicação. Uma interface de linha comando seria um plugin. Uma aplicação Web seria outro plugin. A persistência em um banco de dados relacional, outro plugin. Persistência em um datastore NoSQL, mais um plugin. Integrações com outros sistemas, mais plugins.

Usando interfaces, invertemos as dependências para que todos dependam do domain model, que não depende de ninguém. As dependências sempre apontam na direção das regras de negócio, nunca ao contrário. Cada “mecanismo de entrega” seria um plugin diferente.

Teríamos uma melhor manutenibilidade, extensibilidade e testabilidade, mas ao custo de maior complexidade. Esse estilo arquitetural é detalhado na arquitetura hexagonal e na clean architecture.

No caso do Cotuba, o núcleo da aplicação poderia ficar no módulo cotuba. Extrairíamos a interface de linha de comando para o módulo cotuba.cli. A geração de PDFs para o módulo cotuba.pdf e de EPUBs para o módulo cotuba.epub. Definiríamos interfaces, criando contratos e invertendo as dependências entre esses módulos, sempre apontando na direção do módulo cotuba.

Módulos cotuba.cli, cotuba.pdf e cotuba.epub dependem do módulo cotuba

O desenvolvimento de cada módulo poderia ser feito por diferentes times. As interfaces dos plugins seriam muito importantes para a comunicação entre esses times.

2 Comentários

  1. Rafael Ponte 04/04/2018 at 11:19 #

    Excelente post, Aquiles.

    Embora eu goste da idéia e consiga ver algumas vantagens da arquitetura hexagonal (também conhecida como Package by Feature), eu ainda não consigo ver ela amplamente utilizada numa app corporativa. A abordagem tradicional (Package by Layer) ainda parece ser a mais simples e utilizada pelo mercado, vide apps Java e principalmente nas baseadas em RubyOnRails.

    A verdade é que me falta experiência em um projeto médio-longo prazo para enxergar seus prós e contras de modelo arquitetural. Na minha opinião, alguns frameworks ainda tem limitações e/ou necessitem de mais customizações para trabalhar com essa arquitetura (por exemplo, scanning de classes e entities; CoC, padronização etc).

    Você tem experiências com arquitetura hexagonal?

  2. Alexandre Aquiles 04/04/2018 at 13:49 #

    Rafael,

    Você foi bem ao ponto nessa questão dos frameworks. Em busca de facilidades, nos amarramos e acabamos aprisionados. Acaba ficando difícil separar negócio e aplicação dos mecanismos de entrega. Detalhes vazam para dentro do domínio. No “Clean Architecture”, Uncle Bob fala bastante sobre essa ideia de que os frameworks não são sua arquitetura.

    Mas, no fim das contas, prego uma abordagem pragmática. Faça o código amarrado se for mais fácil mas mantenha consciência do perigo no médio prazo.

    Sobre Package by Feature: discordo que seja a mesma coisa que Hexagonal Architecture (ou Ports And Adapters). Uma HA tem bastante a ver com DDD. Ainda há layers, mas as dependências são invertidas para que seu código de domínio não dependa de infraestrutura. No fim das contas, isolar o seu código de domínio de detalhes técnicos.

Deixe uma resposta