Transformando um Monólito em um Monólito Modular

Transformando um Monólito em um Monólito Modular

Quando se desenvolve em Java, Um dos problemas mais famosos está relacionado ao Classpath, que nada mais é do que uma coleção das classes compiladas que são disponibilizadas nos JARs utilizados pela aplicação. Em tempo de execução, a JVM irá carregar essas classes de maneira dinâmica, ou seja, apenas quando forem solicitadas.

O problema é que não temos garantia que existe apenas uma versão de uma classe nos JARs disponíveis no Classpath ou ainda que a classe carregada é exatamente a que nós precisamos. Isso pode ocasionar uma série de erros, comumente conhecidos como JAR Hell.

A solução veio na JDK 9 com o JPMS (Java Platform Module System). Com o novo sistema de módulos, temos melhoras significativas, pois ao trabalhar com o Module-Path, classes, interfaces, arquivos e dependências ficam vinculadas a um módulo, evitando assim falhas que tínhamos com o Classpath.

Além dessa vantagem, temos uma melhora significativa no encapsulamento das APIs, pois agora tem-se a opção de exportar apenas o que o usuário poderá acessar de cada módulo.

A ideia desse artigo é mostrar, de forma prática, o passo a passo da transformação do Monólito Caelum Eats para um Monólito Modular e deixar claro as vantagens citadas acima.

Antes de iniciar a modularização do projeto, vamos à apresentação do nosso Monólito. O Caelum Eats é uma aplicação de entrega de comida nos moldes de soluções conhecidas no mercado. Basicamente trabalha com 3 perfis de usuários: o cliente, o dono do restaurante e o administrador do sistema. O backend é implementado usando as seguintes tecnologias:

  • Spring Boot
  • Spring Boot Web
  • Spring Boot Validation
  • Spring Boot Actuator
  • Spring Data JPA
  • MySQL Connector/J
  • Flyway DB
  • Spring Security
  • JJWT

O Monólito já está componentizado com módulos Maven, como podemos ver a seguir no pom.xml:

<modules>
    <module>eats-administrativo</module>
    <module>eats-pagamento</module>
    <module>eats-restaurante</module>
    <module>eats-pedido</module>
    <module>eats-distancia</module>
    <module>eats-seguranca</module>
    <module>eats-application</module>
</modules>

Você pode estar se perguntando o porquê de usarmos o JPMS quando já estamos utilizando os módulos Maven. Bom, o módulo Maven não vai resolver o problema do encapsulamento do nosso código. Por exemplo, qualquer outro módulo pode acessar qualquer uma das classes do módulo eats-administrativo, mesmo as de uso interno.

Começando com JPMS

Agora que conhecemos a estrutura do nosso Monólito, vamos colocar a mão na massa e modularizar o nosso projeto. É importante ressaltar que existem várias maneiras de se definir a estratégia dos módulos da aplicação. Uns preferem dividir por camadas, outros por regras de negócio, mas a realidade é que isso vai depender do projeto. Vamos continuar usando a divisão por regras de negócio, que já estava definida no pom.xml raiz.

Vamos criar o arquivo module-info.java para cada módulo Maven, pois é nele que estarão todas as informações referentes ao módulo JPMS. A IDE facilita esse trabalho para nós. No Eclipse, clicando com o botão direito em cima do subprojeto eats-administrativo, pode-se usar o seguinte caminho: Configure - Create module.info.java. O arquivo virá com algumas informações, mas não necessariamente com todas que precisamos.


module br.com.caelum.eats.administrativo { 
        exports br.com.caelum.eats.administrativo.dto;
exports br.com.caelum.eats.administrativo.controller;
    exports br.com.caelum.eats.administrativo.entidade;

    requires java.persistence;
    requires java.validation;
    requires spring.data.commons;
    requires spring.data.jpa;
    requires spring.web;
}

Neste arquivo, existem novas palavras reservadas. Vamos analisar cada uma delas:

  • module: Aqui temos de fato a definição do nosso módulo e, por convenção, usamos o domínio invertido br.com.caelum.eats.administrativo para sua nomenclatura.
  • exports: Como falamos na introdução, para um melhor encapsulamento das nossas APIs, vamos exportar apenas o necessário para o cliente que irá utilizar a API e isso faz-se ao definir os exports. No exemplo acima, estamos falando que os clientes só poderão usar as classes dos pacotes dto, controller e entidade. Qualquer outra classe fora desses pacotes não poderá ser acessada externamente.
  • requires: Precisamos agora informar quais outros módulos vamos usar nos nossos módulos. Esses módulos podem ser da própria plataforma Java/Java EE ou de terceiros, como o Spring.

A criação do module-info.java deve ser feita para os módulos eats-pagamento, eats-restaurante, eats-pedido, eats-distancia, eats-seguranca e eats-application. O conteúdo do arquivo será semelhante.

E agora? Finalizamos?

Modularizar uma aplicação não é uma atividade trivial, ainda mais quando se usa uma quantidade considerável de frameworks. Um detalhe que não foi falado é que quando usamos módulos, automaticamente estamos fechados para reflection e o Spring usa bastante este recurso. Para o nosso sistema funcionar, teremos que fazer mais algumas configurações nos arquivo module-info de cada módulo:

module br.com.caelum.eats.administrativo {
        //resto do código oculto
    opens br.com.caelum.eats.administrativo.entidade to 
org.hibernate.orm.core, spring.core,
org.hibernate.validator;
    opens br.com.caelum.eats.administrativo.controller to 
        spring.context, spring.core, spring.beans;
}

E aqui vemos mais duas novas palavras reservadas. Vamos lá:

  • opens: Aqui estamos definindo que o pacote br.com.caelum.eats.administrativo.entidade está aberto para reflexão. Porém, com o to, nós garantimos que só estará aberto para os módulos org.hibernate.orm.core, spring.core e org.hibernate.validator. O mesmo é feito com o pacote br.com.caelum.eats.administrativo.controller que garantimos estar aberto para reflexão apenas para os módulos spring.context, spring.core e spring.beans. Nenhum outro módulo poderá fazer o uso de reflexão nesses pacotes.

De maneira semelhante, nos arquivos module-info de outros módulos, devemos abrir os pacotes relevantes para os módulos dos frameworks que os utilizam.

Um module-info interessante é o do módulo br.com.caelum.eats:

**open** module br.com.caelum.eats {
        //resto do código oculto
} 

Se vocês perceberem, ao invés de usar o opens por pacote, como no exemplo anterior, é utilizado o open no módulo. Com este recurso, estou abrindo todos os pacotes do módulo para reflexão e sem definição de quais outros módulos poderão fazer tal ação.

Devemos fazer isso porque, ao utilizarmos o Flyway, uma ferramenta de migração de Bancos de Dados, disponibilizamos as migrations no diretório de recursos (src/resources/migration), e não é possível fazer um opens nesse diretório para que o Flyway ache as migrations. Logo a opção foi deixar todo o módulo aberto. Obs: aqui aceito uma sugestão de como fazer para abrir o diretório de migrations para o Flyway.

eats-application
└── src
        └── resources
            ├── application.properties
            └── db
                └── migration
                    ├── V0001__cria-tabelas-user-e-role.sql
                    ├── V0002__cria-tabela-tipo-de-cozinha.sql
                    ├── V0003__cria-tabela-forma-de-pagamento.sql
                    └── V0004__cria-tabela-restaurante.sql

A partir de agora, nossa aplicação está pronta para uso, porém ainda podemos realizar algumas melhorias para tirar proveito de todas as vantagens do JPMS. No momento de fazer os exports também podemos usar o to e informar que apenas alguns módulos específicos poderão usar as classes do pacote exposto. O module-info do módulo Administrativo ficaria assim:

module br.com.caelum.eats.administrativo { 
        exports br.com.caelum.eats.administrativo.dto to
            br.com.caelum.eats.distancia,
            br.com.caelum.eats.pagamento, 
            br.com.caelum.eats.restaurante;
//resto do código oculto
}

Prontos(as) para ver a aplicação funcionando?

Bom, agora que configuramos todos os nossos módulos, precisamos realizar as últimas configurações para executarmos a aplicação. No pom.xml do módulo eats-application, que contém a classe com o método main. É necessário incluir alguns plugins do Maven.

O maven-jar-plugin informa que as classes compiladas estarão disponíveis no diretório /modules:

<plugin>
    <artifactId>maven-jar-plugin</artifactId>
        <configuration>
            <outputDirectory>
    ${project.build.directory}/modules
</outputDirectory>
        </configuration>
</plugin>

O `maven-dependency-plugin`` informa que, no momento do empacotamento, as dependências dos projetos irão ser copiadas, também, para o diretório /modules, pois serão utilizadas em tempo de execução.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration> 
<outputDirectory>
    ${project.build.directory}/modules
</outputDirectory>
                <includeScope>runtime</includeScope>
            </configuration>
        </execution>
    </executions>
</plugin>

Feitos estes passos, estamos prontos para ver a aplicação funcionando.

Na raiz do projeto vamos executar o comando:

mvn install

Se não tivermos nenhum problema, o resultado será o apresentado abaixo:

imagem do código desenvolvido pelo autor

Agora, no diretório /eats-application, vamos executar o comando:

java --add-modules java.instrument
--module-path=target/modules
--module br.com.caelum.eats/br.com.caelum.eats.EatsApplication

As opções do comando anterior:

  • --add-modules : Será utilizado para adicionar módulos em tempo de execução. O módulo java.instrument só será usado nesse momento, logo foi passado como argumento, mas nada impede de adicioná-lo no module-info do módulo eats-application.
  • --module-path : O caminho onde estão disponíveis os módulos gerados pelo build do Maven.
  • --module : O módulo br.com.caelum.eats que contém a classe br.com.caelum.eats.EatsApplication que no nosso caso é a classe main.

Se tudo deu certo, vamos ver a aplicação iniciada:

Started EatsApplication in 14.332 seconds (JVM running for 15.51)

Neste artigo, a ideia era mostrar o passo a passo de como fizemos para modularizar a nossa aplicação. O resultado final vocês podem ver em: https://github.com/joao0212/caelum-eats-modular.

Obviamente aplicações diferentes exigem estratégias diferentes, mas o conhecimento adquirido aqui servirá de base para aplicá-los em outros cenários. Aqui na Alura nós temos o curso Novidades do Java: Produtividade com novos recursos que tem uma aula sobre Java modular.

Bom, chegamos ao fim do nosso artigo e espero que vocês tenham gostado do resultado. Se quiserem expor suas críticas e sugestões, podem me chamar twitter ou no linkedin que ficarei feliz em respondê-los. Até a próxima!

João Victor
João Victor

João Victor é formado em ciências da computação e possui sólidos conhecimentos em desenvolvimento de sistemas utilizando linguagem java.

Veja outros artigos sobre Programação