Migrando para Módulos Java
Estratégias para migrar aplicações Java baseadas em classpath para o Java Platform Module System.
Você não precisa modularizar para migrar para Java 9+. Uma aplicação em classpath roda sem alterações — tudo se torna um único unnamed module. A migração é uma etapa opcional que você realiza quando deseja encapsulamento forte, um runtime customizado com jlink, ou um grafo de dependências mais limpo. A arte está em fazê-la de forma incremental, porque os três tipos de módulos — named, automatic e unnamed — permitem que código modular e não modular coexistam.
Este capítulo aborda quando a migração vale a pena, como fazer uma aplicação existente rodar em uma JDK moderna primeiro, as duas direções possíveis de modularização (bottom-up vs. top-down), as ferramentas que fazem o trabalho pesado, e um exemplo executável que inspeciona o grafo de módulos da mesma forma que o jdeps faz. Se os termos aqui são desconhecidos, comece com a introdução a módulos e declarando um módulo.
Primeiro: apenas rode no Java 9+ (sem módulos)
Antes de adicionar qualquer module-info.java, recompile e execute sua aplicação existente na nova JDK com tudo no classpath. As prováveis falhas não têm nada a ver com seus módulos e sim com a JDK agora modular:
- Módulos Java EE removidos —
java.xml.bind(JAXB),java.activation, CORBA, etc. foram removidos da JDK. Adicione-os de volta como dependências comuns. - Internos encapsulados —
sun.misc.Unsafee similares não são mais acessíveis. As flags--add-exports/--add-openssão uma saída de emergência enquanto você remove o uso deles. - Pacotes divididos — dois JARs contribuindo com o mesmo pacote agora entram em conflito no module path (mas não no classpath).
Deixe a aplicação funcionando no classpath primeiro. Somente então comece a modularizar.
Migração bottom-up
A estratégia preferida quando você controla toda a base de código:
- Comece pelas bibliotecas folha — módulos que não dependem de nada seu.
- Dê a cada uma um
module-info.javae mova-a para o module path. - Os dependentes delas podem agora usar
requirespara referenciá-las. Trabalhe subindo a árvore de dependências em direção ao ponto de entrada da aplicação.
Isso funciona porque um named module pode requerer outros named modules e módulos automatic — portanto, desde que as próprias dependências de uma folha sejam pelo menos automáticas, você pode modularizá-la. Cada passo mantém o build funcionando. Se uma folha expõe comportamento plugável, esse também é um ponto natural para introduzir uma fronteira de serviços para que seus dependentes usem uma interface em vez de uma classe concreta.
Migração top-down
Quando você não controla as bibliotecas (JARs de terceiros sem descritores), faça o caminho inverso:
- Modularize primeiro seu próprio código de nível superior.
- Coloque os JARs de terceiros ainda não modulares no module path, onde se tornam automatic modules.
- Use
requiresreferenciando-os pelo nome automático (derivado do nome do arquivo JAR ou doAutomatic-Module-Name). - À medida que cada biblioteca disponibiliza um
module-info.javareal, substitua a dependência automática pela nomeada — sem alteração no seurequires.
Os automatic modules são o andaime que torna a migração top-down possível; eles permitem que seu código named dependa de JARs que ainda não são modulares.
Ferramentas e armadilhas
jdepsanalisa as dependências reais de um JAR e pode até gerar um rascunho inicial demodule-info.java(jdeps --generate-module-info). Comece por aí em vez de escrever diretivas à mão.Automatic-Module-Name— se você publica uma biblioteca, adicione esta entrada de manifesto antes de escrever um descritor completo. Ela fixa um nome de módulo estável para que usuários de módulos automáticos downstream não sejam quebrados quando você renomear o JAR posteriormente.- Pacotes divididos devem ser mesclados. O module path proíbe dois módulos de possuírem o mesmo pacote; o classpath tolerava isso. Este é o bloqueador de migração mais comum.
openspara reflexão. Frameworks que refletem sobre suas classes (Jackson, JPA, Spring) precisam deopens, ou você obteráInaccessibleObjectExceptionem tempo de execução mesmo que a compilação tenha passado.
Pacotes divididos são a falha que mais surpreende as pessoas. No classpath, dois JARs contendo classes em com.example.util simplesmente se mesclavam; no module path o resolver os rejeita com um erro "module reads package ... from both". Não existe flag que torne isso legal — você deve mesclar o pacote duplicado em um único módulo (ou renomear um deles). Audite pacotes divididos com jdeps antes de começar a escrever descritores.
Um exemplo prático: inspecionando um grafo de dependências como o jdeps
O planejamento de migração começa com "o que depende do quê." Este programa lê os descritores de módulos da camada boot em tempo de execução — a mesma informação de requires que o jdeps apresenta — e também detecta se ele próprio está rodando como um named module ou no classpath, exatamente a verificação que indica até onde uma migração progrediu.
O que observar na execução:
- O programa reportou que estava rodando como módulo UNNAMED — exatamente o que se espera de código em classpath, e o sinal em tempo de execução de que esta base de código não foi modularizada ainda. Executar novamente após adicionar um
module-infoe mover para o module path mudaria isso para NAMED, fornecendo uma verificação concreta de "já chegamos lá?" durante a migração. inspect("java.sql")exibiu sua lista real derequires, incluindo uma dependência transitiva do estilojava.transaction.xaoujava.logging. Essa é a mesma informação que ojdepsapresenta — conhecer as dependências reais de um módulo é o primeiro passo para escrever seumodule-info.java, e o descritor já as contém.- Alguns requires exibiram
(transitive). Essas são as dependências que um módulo re-exporta; quando você usarequires java.sql, você lê automaticamente suas dependências transitivas também. Identificá-las indica quais linhasrequires transitivecopiar ao modularizar código que envolve tal módulo. findModuleretornou umOptional, reforçando que um módulo pode simplesmente não estar presente em um dado runtime — uma imagem compactada comjlinkpode omitirjava.desktopcompletamente. Os planos de migração devem considerar quais módulos realmente são entregues no runtime de destino.- Todo módulo depende fundamentalmente de
java.base, e ele nunca aparece em uma lista derequiresporque é implícito. A contagem total de módulos boot mostra que a JDK é um grafo que pode ser consultado programaticamente — a fundação sobre a qual ferramentas comojdepsejlinkconstroem para analisar e reduzir uma aplicação.
Uma ordem de migração sensata, resumida
- Execute no classpath na nova JDK; corrija falhas de módulos removidos e APIs internas.
- Use
jdepspara mapear dependências e encontrar pacotes divididos. - Adicione
Automatic-Module-Namea quaisquer bibliotecas que você publique. - Modularize bottom-up se você controla a árvore, top-down (apoiando-se em automatic modules) se não controla.
- Adicione
opensonde frameworks refletem; verifique em tempo de execução, não apenas em tempo de compilação.
Isso conclui a parte sobre o Java Platform Module System: agora você sabe o que são módulos, como declarar um, os três tipos e como coexistem, como serviços desacoplam módulos, e como migrar uma aplicação existente para o module path sem uma reescrita.