Pontos Principais
- Implementar aplicações de uma maneira modular estimula boas práticas de design, como a separação de responsabilidades e encapsulamento.
- O Sistema de Módulos da Plataforma Java (JPMS) permite aos desenvolvedores definir quais são os módulos da aplicação, como eles devem ser usados por outros módulos, e de quais outros módulos eles dependem.
- É possível adicionar o JPMS em aplicações que já estão utilizando um sistema diferente para definir os módulos da aplicação, por exemplo: módulos Maven ou subprojetos Gradle.
- O JDK traz ferramentas para ajudar os desenvolvedores a migrar códigos existentes para o JPMS.
- Aplicações ainda podem ter dependências de bibliotecas construídas em versões anteriores ao Java 9. Esses arquivos jar são tratados como um módulo "automático" especial, o que facilita a migração gradual para o Java 9.
Este artigo apresenta um estudo de caso de mudanças necessárias por uma aplicação de maneira a fazer uso do novo Sistema de Módulos da Plataforma Java (JPMS). Note que você não precisa fazer isso para utilizar o Java 9, mas um entendimento do sistema de módulos (frequentemente chamado de Jigsaw) irá, sem dúvida e com o tempo, tornar um habilidade importantes para os desenvolvedores Java.
Vou mostrar os passos necessários para refatorar uma aplicação desenvolvida no Java 8, a qual já foi organizada de uma maneira modular, para utilizar o novo sistema de módulos do Java.
Baixando o Java 9
Primeiramente, precisamos baixar e instalar a versão mais recente do JDK 9. Neste artigo utilizaremos a versão 9-ea+176. Até que o impacto do Java 9 sobre o seu sistema seja entendido, você provavelmente não vai querer que essa seja sua versão padrão do Java. Ao invés de atualizar a variável de ambiente $JAVA_HOME para apontar para a nova instalação, você pode criar uma nova variável de ambiente chamada $JAVA9_HOME. Esta abordagem será utilizada ao longo deste artigo.
Existem vários outros tutoriais que falam a respeito de alguns dos passos necessários para utilizar o Java 9. Nesse artigo iremos focar nossa discussão no componentes de modularização, mas você pode verificar o Guia de Migração da Oracle para informações adicionais.
Modularidade
O que você mais ouvirá a respeito do Java 9 será sobre o Projeto Jigsaw, que introduz um sistema de módulos no Java. Existem vários tutoriais e artigos explicando o que é e como funciona. Este artigo falará sobre como você pode migrar seu código existente para utilizar o novo sistema de módulos do Java (JPMS).
Muitos desenvolvedores ficam surpresos ao saber que eles não precisam adicionar modularidade em código existentes para utilizar o Java 9. O encapsulamento de APIs internas é provavelmente é uma das características que mais preocupam os desenvolvedores quando consideram utilizar o Java 9. Mas só porque esta parte do projeto Jigsaw pode impactar os desenvolvedores não significa que desenvolvedores precisar entender completamente sobre modularidade para utilizar o Java 9.
Se você quiser tirar vantagem do sistema de módulos do Java (JPMS), existem ferramentas para lhe ajudar, como por exemplo o analisador de dependências jdeps, o compilador Java e sua própria IDE.
Não escreverei a respeito de como decompor sua aplicação em módulos, isso pode ser difícil de conseguir se não for feito no começo. Veja como exemplo os vários anos que levaram para organizar o JDK em módulos! Ao invés disso, assumirei que sua aplicação já está estruturada em pequenas partes, possivelmente usando módulos do Maven ou subprojetos Gradle, ou talvez utilizando sub projetos ou módulos na IDE.
Você encontrará muitos tutoriais, como por exemplo o guia rápido do projeto Jigsaw, o qual assume que a estrutura do projeto irá parecer como na Figura 1.
Figura 1
A estrutura tem um único diretório de código fonte e testes, e todos os módulos são sub diretório destes. Esta é uma diferença das estruturas de projeto do Maven ou Gradle que estamos acostumados, onde cada módulo contém seu próprio diretório de código fonte e testes. Ainda bem que você não tem que reorganizar sua aplicação inteira desta forma (acompanhada da dor de cabeça de ter que fazer suas ferramentas de build entender esta atualização na estrutura). Você pode continuar com a estrutura de pastas do Maven/Gradle e ainda adotar o JPMS, assim que você entender a diferença na estrutura que existem nos vários tutoriais. O ponto chave é saber quais diretórios são a raiz do código fonte da sua aplicação. Na estrutura do Maven/Gradle, são as pastas src e test.
Figura 2
A primeira coisa que você precisará fazer será criar um arquivo chamado module-info.java no diretório do código fonte raiz do seu módulo, definindo o nome do módulo. Você pode criar isto manualmente, ou a IDE pode criar para você. A Figura 3 mostra um arquivo modulo-info.java simples para o modulo de serviço deste tutorial.
Figura 3
Note que o arquivo foi criado no diretório src, e dentro pasta que é a raiz da estrutura de diretório.
Agora compile o projeto na sua IDE ou vá até o diretório src na linha de comando e compile o módulo com o seguinte comando.
> "%JAVA9_HOME%"\bin\javac -d ..\mods\service module-info.java com\mechanitis\demo\sense\service\*.java com\mechanitis\demo\sense\service\config\*.java
Ao executar o comando, você verá vários erros de compilação (veja a Figura 4).
Figura 4
Existem 27 erros, os quais podem parecer uma surpresa. Afinal, este projeto estava compilando e funcionando perfeitamente. Tudo que nós fizemos foi adicionar um arquivo chamado module-info.java, e agora ele nem compila. A razão é que nós temos que ser mais explícitos com respeito aos módulos que queremos que nossos módulos utilizem. Estes módulos obrigatórios podem ser: módulos da JDK, outros módulos que nós mesmos criamos, ou módulos de dependências externas, que provavelmente serão módulos automáticos.
Aqui, o analisador de dependências jdeps pode nos ajudar a identificar os módulos que vamos precisar declarar no arquivo module-info.java. Antes de rodar o jdeps com sucesso, será necessário fazer algumas modificações:
- O arquivo jar do módulo (pré-JPMS), ou um diretório contendo os arquivos .class. Note que se você acabou de tentar compilar o projeto no último passo, você não terá nenhum arquivo .class. Será preciso remover o arquivo module-info.java e recompilar.
- O classpath para seus módulos. Se você está acostumado a rodar o projeto pela IDE, especialmente se você estiver usando Maven ou Gradle para gerenciar as dependências, este classpath pode ser um desafio para encontrar e criar. No InlelliJ IDEA, você pode identificar um classpath que esteja funcionado olhando os detalhes na janela do console que exibe os logs da aplicação ou do testes. Na Figura 5, precisei apenas rolar a barra de rolagem para a direita e copiar o resto da linha em azul.
Figura 5
Agora temos o que precisamos para rodar o jdeps, iremos usar o jdeps na versão 9 do Java com as parâmetros apropriados.
> "%JAVA9_HOME%"\bin\jdeps --class-path %SERVICE_MODULE_CLASSPATH% out\production\com.mechanitis.demo.sense.service
O último parâmetro é o diretório contendo os arquivos .class para o módulo de serviço. Quando executo o comando, obtemos a saída abaixo.
split package: javax.annotation [jrt:/java.xml.ws.annotation, C:\.m2\...\javax.annotation-api-1.2.jar] com.mechanitis.sense.service -> java.base com.mechanitis.sense.service -> java.logging com.mechanitis.sense.service -> C:\.m2\...\javax-websocket-server-impl-9.4.6.jar com.mechanitis.sense.service -> C:\.m2\...\javax.websocket-api-1.0.jar com.mechanitis.sense.service -> C:\.m2\...\jetty-server-9.4.6.jar com.mechanitis.sense.service -> C:\.m2\...\jetty-servlet-9.4.6.jar com.mechanitis.sense.service -> com.mechanitis.sense.service.config com.mechanitis.sense.service com.mechanitis.sense.service -> java.io java.base com.mechanitis.sense.service -> java.lang java.base com.mechanitis.sense.service -> java.lang.invoke java.base com.mechanitis.sense.service -> java.net java.base com.mechanitis.sense.service -> java.nio.file java.base com.mechanitis.sense.service -> java.util java.base com.mechanitis.sense.service -> java.util.concurrent java.base com.mechanitis.sense.service -> java.util.concurrent.atomic java.base com.mechanitis.sense.service -> java.util.function java.base com.mechanitis.sense.service -> java.util.logging java.logging com.mechanitis.sense.service -> java.util.stream java.base com.mechanitis.sense.service -> javax.websocket javax.websocket-api-1.0.jar com.mechanitis.sense.service -> javax.websocket.server javax.websocket-api-1.0.jar com.mechanitis.sense.service -> org.eclipse.jetty.server jetty-server-9.4.6.jar com.mechanitis.sense.service -> org.eclipse.jetty.servlet jetty-servlet-9.4.6.jar com.mechanitis.sense.service -> org.eclipse.jetty.websocket.server javax-websocket-server-impl-9.4.6.jar com.mechanitis.sense.service -> org.eclipse.jetty.websocket.server.deploy javax-websocket-server-impl-9.4.6.jar com.mechanitis.sense.service.config -> java.lang java.base com.mechanitis.sense.service.config -> java.lang.invoke java.base com.mechanitis.sense.service.config -> javax.websocket javax.websocket-api-1.0.jar com.mechanitis.sense.service.config -> javax.websocket.server javax.websocket-api-1.0.jar
Uma mensagem de aviso é exibida informando sobre um pacote duplicado, que é o que acontece quando um nome de pacote aparece em dois módulos ou arquivos jars. Neste caso, é porque o mesmo pacote java.xml.ws.annotation existe tanto na biblioteca padrão da JDK quanto no arquivo jar javax.annotation-api.jar. E então é exibido todos os pacotes que o módulo de serviço utiliza, e em qual módulo ou arquivos jars estes pacotes se encontram. Podemos utilizar essa informação para criar o arquivo module-info.java que irá funcionar para o módulo de serviço:
module com.mechanitis.demo.sense.service { requires java.logging; requires javax.websocket.api; requires jetty.server; requires jetty.servlet; requires javax.websocket.server.impl; }
O pacote java.base contém a maior parte dos itens básicos da JDK, mas não precisamos incluí-lo, eles são incluídos por padrão. Precisamos, no entanto, saber os nomes dos módulos automáticos das dependências externas.
Módulos Automáticos
O Java 9 e a JPMS foram elaborados para levar em consideração o fato de que a maior parte de códigos existentes, particularmente bibliotecas que todos nós usamos, provavelmente não usam JPMS (isto é, não são totalmente modulares, com o arquivo module-info.java definindo as dependências e permissões). Para cobrir a lacuna e tornar a migração mais fácil, seu código modularizado ainda pode conter dependências que não são de fato modulares. Eles serão tratados como módulos automáticos: módulos especiais onde você ainda pode acessar todos os pacotes dentro do jar exatamente da mesma forma como faz originalmente, quando você inclui o arquivo jar no classpath. A única complicação para nós desenvolvedores, ao utilizar módulos automáticos, é saber qual o nomes desses módulos. Por padrão, o nome é mais ou menos o nome do arquivo jar, sem qualquer número de versão. Por exemplo jetty-server-9.4.1.v20170120.jar torna-se um módulo automático chamado jetty.server (perceba o uso de pontos ao invés de hífens).
Nota: As versões mais recentes do Java 9 permitem aos desenvolvedores especificar o nome do módulo automático dentro do arquivo MANIFEST do arquivo jar através do atributo 'Automatic-Module-Name'. Talvez você precise verificar o arquivo jar para saber qual o nome do módulo.
Utilizando Nosso novo Módulo
Agora nosso código compila corretamente. Podemos nos focar no trabalho de migração dos outros módulos para torná-los compatíveis com o JPMS. Vamos criar um arquivo module-info.java vazio para o módulo de usuário, a Figura 6 mostra o novo erro de compilação.
Figura 6
Este erro é simples de corrigir, precisamos apenas atualizar o arquivo module-info.java do módulo de serviço para permitir que outros módulos acessem o pacote, fazemos isso o exportando:
module com.mechanitis.demo.sense.service { requires java.logging; requires javax.websocket.api; requires jetty.server; requires jetty.servlet; requires javax.websocket.server.impl; exports com.mechanitis.demo.sense.service; }
Ao compilar novamente, temos um outro erro:
Error:(3, 33) java: package com.mechanitis.demo.sense.service is not visible (package com.mechanitis.demo.sense.service is declared in module com.mechanitis.demo.sense.service, but module com.mechanitis.demo.sense.user does not read it)
Isso significa que precisamos atualizar o módulo de usuário e incluir a dependência do módulo de serviço:
module com.mechanitis.demo.sense.user { requires com.mechanitis.demo.sense.service; }
Feito isso, tudo compila e podemos rodar nosso serviço de usuário com sucesso.
Conclusão
Vimos o processo de refatorar uma aplicação existente para utilizar o Sistema de Módulos da Plataforma Java (JPMS). Embora existam benefícios de dividir uma aplicação em módulos, migrar aplicações existente para o JPMS não é uma tarefa fácil e devemos apenas tentar fazê-lo se isso realmente trouxer benefícios para a aplicação.
About the Author
Trisha Gee é Java Champion e já desenvolveu aplicações Java para empresas de todos os tamanhos e de diversas áreas, incluindo a área financeira, industrial, de tecnologia e empresas sem fins lucrativos. Com uma grande experiência em alta performance em aplicações Java, ela é apaixonada por potencializar a produtividade de desenvolvedoras(es) com desenvolvimento open source. Trisha é Java Developer Advocate na JetBrains, o que é uma perfeita desculpa para viver em constante contato com as novidades mais inovadoras da tecnologia Java.