Como declarar o que cada parte de um programa oferece e de que partes ela depende, e como a JVM usa isso para proteger o interior do código e montar programas enxutos.
"Construção Automatizada" terminou apontando uma fragilidade que nenhuma ferramenta de build resolve. No classpath, qualquer classe pública de qualquer JAR é visível para todas as outras. Um pacote pensado como detalhe interno, como um interno ou um impl, acaba usado por outros projetos, e mudar esse detalhe depois quebra quem o usou. Uma dependência esquecida só aparece como NoClassDefFoundError, com o programa já rodando. Desde o Java 9, a plataforma tem uma resposta para isso: o sistema de módulos, conhecido pela sigla JPMS (Java Platform Module System). Nesta aula você vai transformar o projeto de tarefas em módulos, ver as fronteiras sendo aplicadas pelo compilador e pela JVM, conhecer o mecanismo de serviços e gerar um ambiente de execução sob medida para o programa.
O que é um módulo
Um módulo é um grupo de pacotes com um nome e uma declaração que diz duas coisas:
- o que ele exporta: quais pacotes outros módulos podem usar. Os não exportados ficam invisíveis de fora, mesmo que suas classes sejam
public; - o que ele requer: de quais outros módulos ele depende.
A declaração fica num arquivo especial, module-info.java, na raiz dos fontes do módulo. Com ele, o public deixa de significar "visível para todo mundo" e passa a significar "visível para quem pode ler este pacote". O encapsulamento de "O que Fica Dentro, o que Fica Fora", que protegia os campos dentro das classes, sobe um nível e passa a proteger pacotes inteiros dentro dos módulos.
O próprio JDK foi dividido em módulos no Java 9. O comando java --list-modules lista todos eles, como java.base, java.sql e java.desktop, e é possível inspecionar qualquer um deles:
$ java --describe-module java.sql
java.sql@25.0.4.1
exports java.sql
exports javax.sql
requires java.base mandated
requires java.transaction.xa transitive
requires java.logging transitive
requires java.xml transitive
uses java.sql.Driver
O módulo java.base, com java.lang, java.util, java.io e as demais classes fundamentais, é requerido implicitamente por todos os outros (o mandated da saída). As palavras transitive e uses aparecem mais adiante nesta aula.
O projeto de tarefas em módulos
O projeto de "Do Arquivo Solto ao Projeto" vai ser dividido em dois módulos: um núcleo, com o modelo e o serviço, reutilizável por qualquer interface, e uma aplicação, com o programa de linha de comando. Para mostrar o encapsulamento, o núcleo ganha um pacote interno, com uma classe que só ele deve usar:
package br.com.exemplo.tarefas.interno;
import java.time.LocalDate;
public class Relogio {
public static LocalDate hoje() {
return LocalDate.of(2026, 9, 25);
}
}
Com o compilador puro, sem ferramenta de build, a convenção é uma pasta por módulo, com o nome do módulo:
src/
├── br.com.exemplo.tarefas/
│ ├── module-info.java
│ └── br/com/exemplo/tarefas/
│ ├── modelo/Tarefa.java
│ ├── servico/ListaDeTarefas.java
│ └── interno/Relogio.java
└── br.com.exemplo.tarefas.app/
├── module-info.java
└── br/com/exemplo/tarefas/app/Principal.java
O nome de um módulo segue a mesma convenção do domínio invertido dos pacotes, e costuma ser o nome do pacote principal. A declaração do núcleo:
module br.com.exemplo.tarefas {
exports br.com.exemplo.tarefas.modelo;
exports br.com.exemplo.tarefas.servico;
}
O pacote interno não aparece, e por isso fica fechado. A declaração da aplicação:
module br.com.exemplo.tarefas.app {
requires br.com.exemplo.tarefas;
}
Para compilar os dois de uma vez, a opção --module-source-path indica a pasta que contém os módulos, e --module lista quais compilar:
javac -d mods --module-source-path src --module br.com.exemplo.tarefas,br.com.exemplo.tarefas.app
Cada módulo compilado vai para a sua própria pasta dentro de mods, com um module-info.class. Para executar, os módulos são procurados no module path, com a opção --module-path (ou -p), e a classe principal é indicada com o módulo, com a opção --module (ou -m):
java --module-path mods --module br.com.exemplo.tarefas.app/br.com.exemplo.tarefas.app.Principal
Pendentes:
#2 Revisar genéricos
#3 Fazer os exercícios
Atrasada há 3 dias: Revisar genéricos
As fronteiras em ação
Agora as regras são aplicadas. Se a aplicação tentar usar o relógio interno do núcleo:
import br.com.exemplo.tarefas.interno.Relogio;
Principal.java:7: error: package br.com.exemplo.tarefas.interno is not visible
import br.com.exemplo.tarefas.interno.Relogio;
^
(package br.com.exemplo.tarefas.interno is declared in module br.com.exemplo.tarefas, which does not export it)
Relogio é uma classe public, com um método public, e mesmo assim não pode ser usada de fora do módulo. O autor do núcleo pode reescrever, renomear ou apagar o pacote interno sem medo de quebrar ninguém.
Se a aplicação esquecer de declarar a dependência, com um module-info.java vazio:
Principal.java:3: error: package br.com.exemplo.tarefas.modelo is not visible
import br.com.exemplo.tarefas.modelo.Tarefa;
^
(package br.com.exemplo.tarefas.modelo is declared in module br.com.exemplo.tarefas, but module br.com.exemplo.tarefas.app does not read it)
As duas mensagens mostram as duas condições para que um código use um pacote de outro módulo: o módulo dono precisa exportar o pacote, e o módulo que usa precisa ler o dono, declarando requires. As duas coisas são verificadas na compilação, e de novo quando a JVM inicia.
Esta segunda verificação é a que resolve o NoClassDefFoundError de "Do Arquivo Solto ao Projeto". Com módulos, a JVM confere, antes de executar a primeira linha, se todos os módulos requeridos estão presentes. Se faltar um, o programa nem começa, com uma mensagem que diz qual módulo falta (java.lang.module.FindException: Module ... not found). Com o classpath, a falta só apareceria quando o código chegasse ao ponto que usa a classe ausente, talvez minutos, dias ou meses depois.
JARs modulares
Um módulo pode ser empacotado num JAR comum, que passa a ser um JAR modular, porque contém um module-info.class:
jar --create --file jars/tarefas.jar -C mods/br.com.exemplo.tarefas .
jar --create --file jars/tarefas-app.jar --main-class br.com.exemplo.tarefas.app.Principal \
-C mods/br.com.exemplo.tarefas.app .
java -p jars -m br.com.exemplo.tarefas.app
Como o JAR da aplicação tem a classe principal registrada, basta o nome do módulo no -m. A ferramenta jdeps analisa as dependências reais entre JARs e módulos:
$ jdeps --module-path jars -s jars/tarefas-app.jar jars/tarefas.jar
br.com.exemplo.tarefas -> java.base
br.com.exemplo.tarefas.app -> br.com.exemplo.tarefas
br.com.exemplo.tarefas.app -> java.base
O jdeps também funciona com JARs não modulares, e é a primeira ferramenta a usar para descobrir de quais módulos do JDK um programa antigo precisa.
requires transitive
Suponha que o núcleo tivesse um método público que devolve um tipo de outro módulo, como um java.sql.Timestamp. Quem usa o núcleo precisaria ler também o módulo java.sql para usar o resultado. Obrigar cada cliente a declarar essa dependência seria frágil. A solução é requires transitive:
module br.com.exemplo.tarefas {
requires transitive java.sql;
exports br.com.exemplo.tarefas.modelo;
}
Todo módulo que requer o núcleo passa a ler java.sql automaticamente. A regra prática: use requires transitive para os módulos cujos tipos aparecem na sua API pública, e requires simples para os que você usa só internamente. Foi isso que a saída de --describe-module java.sql mostrou: o java.sql requer transitivamente java.logging e java.xml, porque tipos desses módulos aparecem na API dele.
Reflexão e opens
Em "O Programa que se Examina", a reflexão conseguia alterar um campo privado de uma classe comum, mas não um de String, porque o módulo java.base não se abre. Com módulos próprios, a mesma regra vale para o seu código. Se a aplicação tentar acessar por reflexão o campo privado tarefas da ListaDeTarefas:
InaccessibleObjectException: Unable to make field private final java.util.List
br.com.exemplo.tarefas.servico.ListaDeTarefas.tarefas accessible: module br.com.exemplo.tarefas
does not "opens br.com.exemplo.tarefas.servico" to module br.com.exemplo.tarefas.app
exports libera o acesso ao que é público, na compilação e na execução. O acesso por reflexão aos membros privados exige outra declaração, opens:
module br.com.exemplo.tarefas {
exports br.com.exemplo.tarefas.modelo;
exports br.com.exemplo.tarefas.servico;
opens br.com.exemplo.tarefas.servico to br.com.exemplo.tarefas.app;
}
A forma com to abre o pacote só para os módulos listados, e a forma sem to abre para todos. Isso importa na prática porque frameworks que preenchem objetos por reflexão, como os conversores de JSON e as bibliotecas de acesso a banco de dados da trilha de web, precisam que os pacotes com as classes de dados estejam abertos para eles. Quando um framework falha com InaccessibleObjectException num projeto modular, a solução quase sempre é um opens no pacote certo, para o módulo do framework.
Serviços: interfaces e implementações desacopladas
Os módulos têm também um mecanismo para o padrão de "Contratos sem Implementação", de depender de interfaces e não de implementações. Um módulo pode declarar que usa uma interface, e outro, que a fornece, sem que os dois se conheçam.
O núcleo ganha uma interface de exportação de tarefas:
package br.com.exemplo.tarefas.servico;
import br.com.exemplo.tarefas.modelo.Tarefa;
import java.util.List;
public interface Exportador {
String formato();
String exportar(List<Tarefa> tarefas);
}
Um terceiro módulo, br.com.exemplo.tarefas.csv, fornece uma implementação:
package br.com.exemplo.tarefas.csv;
import br.com.exemplo.tarefas.modelo.Tarefa;
import br.com.exemplo.tarefas.servico.Exportador;
import java.util.List;
import java.util.stream.Collectors;
public class ExportadorCsv implements Exportador {
@Override
public String formato() {
return "csv";
}
@Override
public String exportar(List<Tarefa> tarefas) {
return tarefas.stream()
.map(t -> t.id() + ";" + t.titulo() + ";" + (t.prazo() == null ? "" : t.prazo()))
.collect(Collectors.joining("\n", "id;titulo;prazo\n", ""));
}
}
module br.com.exemplo.tarefas.csv {
requires br.com.exemplo.tarefas;
provides br.com.exemplo.tarefas.servico.Exportador
with br.com.exemplo.tarefas.csv.ExportadorCsv;
}
Repare que o pacote csv não é exportado. Ninguém precisa conhecer a classe ExportadorCsv. A aplicação declara que usa a interface:
module br.com.exemplo.tarefas.app {
requires br.com.exemplo.tarefas;
uses br.com.exemplo.tarefas.servico.Exportador;
}
E pede as implementações disponíveis ao ServiceLoader:
ServiceLoader<Exportador> exportadores = ServiceLoader.load(Exportador.class);
int encontrados = 0;
for (Exportador e : exportadores) {
encontrados++;
IO.println("--- formato " + e.formato() + " ---");
IO.println(e.exportar(lista.pendentes()));
}
if (encontrados == 0) {
IO.println("Nenhum exportador instalado.");
}
Com os três módulos no module path:
--- formato csv ---
id;titulo;prazo
2;Revisar genéricos;2026-09-22
3;Fazer os exercícios;
Sem o módulo csv no module path, o mesmo programa, sem nenhuma alteração, imprime "Nenhum exportador instalado.". A aplicação não requer o módulo csv, nem conhece a classe que ele contém. A JVM encontra os fornecedores ao montar os módulos, e o ServiceLoader os entrega. Acrescentar um formato novo é criar um módulo novo com um provides e colocá-lo no module path. É um sistema de plugins embutido na plataforma, e é assim que o próprio JDK encontra os drivers de banco de dados, no uses java.sql.Driver que apareceu na descrição do módulo java.sql.
Uma JVM sob medida: jlink
Como cada módulo declara de que módulos depende, é possível calcular exatamente quais módulos do JDK um programa precisa, e montar um ambiente de execução só com eles. A ferramenta jlink faz isso:
jlink --module-path mods --add-modules br.com.exemplo.tarefas.app --output imagem \
--launcher tarefas=br.com.exemplo.tarefas.app/br.com.exemplo.tarefas.app.Principal
A pasta imagem contém uma JVM completa, com os módulos do programa e apenas os módulos do JDK de que eles precisam, e um script bin/tarefas que inicia o programa:
$ ./imagem/bin/java --list-modules
br.com.exemplo.tarefas
br.com.exemplo.tarefas.app
java.base@25.0.4.1
$ ./imagem/bin/tarefas
Pendentes:
#2 Revisar genéricos
...
Na máquina de teste, a imagem ocupou cerca de 60 MB, contra 330 MB do JDK completo instalado, e roda sem que o usuário tenha nenhum Java instalado. É a razão de a Oracle ter deixado de distribuir um JRE separado, como vimos em "Do Código ao Bytecode": cada programa leva o seu próprio ambiente de execução, do tamanho certo. Otimizações de tamanho, instaladores nativos e a compilação para código de máquina são o assunto de "Do JAR ao Executável".
Módulos na prática
Programas não modulares continuam funcionando, e a maioria dos projetos Java ainda não usa módulos. A convivência funciona assim:
- Tudo o que está no classpath forma o módulo sem nome (unnamed module), que lê todos os outros módulos e exporta todos os seus pacotes. É o comportamento antigo, preservado. Os mesmos arquivos
.classdo projeto de tarefas rodam comjava -cpexatamente como antes, ignorando osmodule-info. - Um JAR comum colocado no module path vira um módulo automático, com um nome derivado do nome do arquivo (ou do atributo
Automatic-Module-Namedo manifesto), que exporta todos os pacotes e lê todos os módulos. É a ponte para usar bibliotecas antigas num projeto modular.
Quando adotar módulos? Para bibliotecas, declarar um módulo, ou pelo menos um Automatic-Module-Name, é uma cortesia com quem as usa, e a forma mais segura de esconder os pacotes internos. Para aplicações, os ganhos principais são o encapsulamento forte entre as partes de um sistema grande e as imagens enxutas do jlink. O custo é a configuração extra, e o atrito com bibliotecas e frameworks que dependem de reflexão, que exigem opens bem pensados. Muitos projetos corporativos, incluindo boa parte dos projetos Spring, rodam no classpath. O importante é que, mesmo sem usar módulos no seu código, você já depende deles: foram eles que fecharam o interior do JDK e que produzem os erros de InaccessibleObjectException que você vai encontrar.
O projeto de tarefas agora tem pacotes, build automatizado e fronteiras entre as suas partes. O que ele ainda não tem é uma garantia de que funciona. Cada exemplo desta fase foi verificado à mão, rodando o programa e lendo a saída, uma verificação que ninguém vai repetir a cada mudança. Em "O Programa que se Examina" você construiu um pequeno executor de testes com reflexão, uma versão simplificada das ferramentas que fazem esse trabalho de verdade.
Fontes e leituras recomendadas
- JEP 261, "Module System": https://openjdk.org/jeps/261. A especificação completa do sistema de módulos: declarações, module path, módulos automáticos e o módulo sem nome.
- Mark Reinhold, "The State of the Module System" (2016): https://openjdk.org/projects/jigsaw/spec/sotms/. O texto do arquiteto da plataforma que explica os objetivos e o funcionamento dos módulos, com exemplos. É a melhor introdução conceitual.
- Dev.java, "Introduction to Modules in Java": https://dev.java/learn/modules/intro/. O tutorial oficial moderno, com seções sobre serviços, reflexão e módulos automáticos.
- Documentação de
ServiceLoader(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ServiceLoader.html. O mecanismo de serviços, com a forma modular e a antiga, baseada em arquivosMETA-INF/services. - Documentação da ferramenta
jlink: https://docs.oracle.com/en/java/javase/25/docs/specs/man/jlink.html. As opções de montagem de imagens, incluindo as de compressão e remoção de informações de depuração.
Exercícios
Exercício 1
Uma biblioteca de pagamentos tem os pacotes br.com.pag.api (as interfaces que os clientes usam), br.com.pag.impl (as implementações) e br.com.pag.util (utilitários internos). Ela usa classes de java.net.http internamente, e alguns métodos da API devolvem java.time.LocalDate. Escreva o module-info.java.
Ver resposta
✓ Resposta:
module br.com.pag {
requires java.net.http;
exports br.com.pag.api;
}
Só api é exportado, e impl e util ficam fechados, podendo mudar à vontade. java.net.http é requerido sem transitive, porque é usado só internamente, e nenhum tipo dele aparece na API. java.time está no módulo java.base, que todo módulo lê implicitamente, então nenhuma declaração é necessária para ele. Se a API devolvesse algum tipo de java.net.http, como um HttpResponse, a dependência precisaria ser requires transitive java.net.http, para que os clientes pudessem usar o resultado. Se as implementações de impl devessem ser encontradas via ServiceLoader, o módulo também declararia provides ... with ....
Exercício 2
Explique a diferença entre as duas mensagens de erro da aula, "which does not export it" e "does not read it", e diga qual módulo precisa ser alterado em cada caso.
Ver resposta
✓ Resposta: "which does not export it" significa que o módulo dono do pacote não o exportou. A decisão é do dono: se o pacote deve ser usado de fora, o module-info dele precisa de exports. Se é interno, o erro está em quem tentou usá-lo, que deve usar outra coisa. "does not read it" significa que o pacote é exportado, mas o módulo que tenta usá-lo não declarou a dependência. A correção é no module-info do módulo usuário, com um requires. As duas condições são necessárias ao mesmo tempo: exportar é o dono permitindo, e ler é o usuário declarando que depende. Uma boa forma de lembrar: exports fala sobre o que eu ofereço, e requires, sobre o que eu preciso.
Exercício 3
O módulo de tarefas passa a ter na classe Tarefa um método público java.sql.Timestamp criadaEm(). O núcleo declara requires java.sql;. O que acontece ao compilar a aplicação, que chama t.criadaEm().getTime(), e como corrigir da forma certa?
Ver resposta
✓ Resposta: A aplicação lê o núcleo, mas não lê java.sql. Ao usar o resultado de criadaEm(), que é um Timestamp, o compilador recusa a chamada: Timestamp.getTime() in package java.sql is not accessible, porque o módulo da aplicação não lê java.sql. Compilado com -Xlint:exports, o próprio núcleo já recebe um aviso de que Timestamp aparece na sua API sem requires transitive. A correção errada é acrescentar requires java.sql na aplicação: funciona, mas obriga todo cliente do núcleo a conhecer um detalhe dele, e quebra de novo se o tipo mudar. A correção certa é no núcleo, com requires transitive java.sql;, porque o Timestamp faz parte da API pública do núcleo, e quem lê o núcleo precisa automaticamente ler o módulo desse tipo. Uma correção ainda melhor seria questionar o projeto: java.sql.Timestamp é um tipo antigo, e a API poderia devolver um Instant, de java.time, que está em java.base e não exige dependência nenhuma, como sugeriu "Tempo, Dinheiro e Padrões".
Exercício 4
Crie um módulo br.com.exemplo.tarefas.markdown que forneça um Exportador no formato de lista Markdown (- [ ] titulo). O que precisa mudar na aplicação para que ela passe a exportar nos dois formatos?
Ver resposta
✓ Resposta:
package br.com.exemplo.tarefas.markdown;
import br.com.exemplo.tarefas.modelo.Tarefa;
import br.com.exemplo.tarefas.servico.Exportador;
import java.util.List;
import java.util.stream.Collectors;
public class ExportadorMarkdown implements Exportador {
@Override
public String formato() {
return "markdown";
}
@Override
public String exportar(List<Tarefa> tarefas) {
return tarefas.stream()
.map(t -> "- [" + (t.concluida() ? "x" : " ") + "] " + t.titulo())
.collect(Collectors.joining("\n"));
}
}
module br.com.exemplo.tarefas.markdown {
requires br.com.exemplo.tarefas;
provides br.com.exemplo.tarefas.servico.Exportador
with br.com.exemplo.tarefas.markdown.ExportadorMarkdown;
}
Na aplicação, nada precisa mudar: nem o código, nem o module-info. Basta compilar o módulo novo e colocá-lo no module path junto com os outros, e o ServiceLoader passa a entregar os dois exportadores. É o objetivo do mecanismo de serviços: a aplicação depende só do contrato Exportador, declarado no núcleo, e as implementações são descobertas quando a aplicação roda. O princípio aberto-fechado de "Um Tipo, Várias Formas" aplicado a módulos inteiros.
Exercício 5
Um colega defende que "todo projeto Java moderno deveria usar módulos". Apresente um argumento a favor e um contra, e diga em que situação você recomendaria cada caminho.
Ver resposta
✓ Resposta: A favor: os módulos dão encapsulamento forte entre as partes de um sistema, verificado pelo compilador e pela JVM. Pacotes internos ficam realmente internos, dependências faltantes são detectadas na inicialização e não durante a execução, e o jlink produz ambientes de execução enxutos. Em sistemas grandes, com várias equipes, as fronteiras explícitas evitam que o código se transforme num emaranhado de dependências entre pacotes. Contra: muitas bibliotecas e frameworks ainda não publicam módulos adequados, ou dependem de reflexão de formas que exigem muitos opens, e o atrito de configuração pode ser grande. Em aplicações que rodam num servidor com um JDK completo, o ganho do jlink é menor. Recomendação: para bibliotecas, sempre declarar ao menos um nome de módulo, e de preferência um module-info completo. Para aplicações novas e autocontidas, como ferramentas de linha de comando e aplicativos de desktop, módulos compensam. Para aplicações que dependem fortemente de frameworks baseados em reflexão, o classpath continua sendo a escolha pragmática, e o encapsulamento pode ser buscado com outras ferramentas, como o ArchUnit, uma biblioteca que verifica em testes as regras de dependência entre pacotes.