Java Módulos (JPMS)

Desde o Java 9, além de classes e pacotes, o Java tem um nível acima: o módulo. Um módulo é um grupo de pacotes com um "contrato" que diz de quais outros módulos ele precisa e quais dos seus pacotes os outros podem usar. O próprio JDK foi dividido em módulos, e é isso que permite ao jlink montar um Java enxuto, só com o que o seu programa usa.

Você vai aprender
  • Que problema os módulos resolvem.
  • Escrever um module-info.java com requires e exports.
  • Compilar e rodar com --module-path e --module.
  • Entender os erros is not visible.
  • Descobrir dependências com jdeps.
  • Criar um Java sob medida com jlink (e ver o tamanho).
  • Quando vale a pena usar módulos (spoiler: na maioria dos projetos, não).

JPMS = Java Platform Module System, "sistema de módulos da plataforma Java". Pré-requisitos: Pacotes e import, Modificadores de Acesso e a aula de JAR (classpath).


A ideia: o prédio com portaria

Pense num prédio comercial. Cada empresa (um pacote) tem as suas salas. Antes dos módulos, o Java era um prédio sem portaria: quem entrasse no classpath andava por todas as salas public de todas as empresas, inclusive as que eram só para uso interno. Os módulos acrescentam uma portaria em cada andar:

  • exports: "estas salas recebem visitas" (pacotes que outros módulos podem usar);
  • requires: "este andar precisa visitar aquele outro" (de quais módulos eu dependo).

Tudo o que não é exportado fica escondido, mesmo que as classes sejam public. E as dependências ficam escritas: o Java confere, antes de rodar, se tudo de que o programa precisa está presente (em vez de quebrar no meio com um NoClassDefFoundError, como você viu na aula de JAR).

O JDK é feito de módulos

$ java --list-modules | wc -l
69
$ java --list-modules | head -12
java.base@25.0.4.1
java.compiler@25.0.4.1
java.datatransfer@25.0.4.1
java.desktop@25.0.4.1
java.instrument@25.0.4.1
java.logging@25.0.4.1
java.management@25.0.4.1
java.management.rmi@25.0.4.1
java.naming@25.0.4.1
java.net.http@25.0.4.1
java.prefs@25.0.4.1
java.rmi@25.0.4.1

São 69 módulos. O mais importante é o java.base, com o essencial (java.lang, java.util, java.io…). Outros são especializados: java.logging (logs), java.desktop (janelas), java.sql (bancos de dados). Para ver o que um módulo exporta:

$ java --describe-module java.base | head -5
java.base@25.0.4.1
exports java.io
exports java.lang
exports java.lang.annotation
exports java.lang.classfile

1. Um projeto com dois módulos

A Ana reorganizou o caixa da aula de JAR em dois módulos:

  • br.com.ana.util: utilidades. Tem o pacote br.com.ana.util (a classe Moeda, para todo mundo usar) e o pacote br.com.ana.util.interno (a classe Arredonda, um detalhe interno que ela não quer que ninguém use diretamente).
  • br.com.ana.loja: o programa, com a classe Caixa.

A convenção é uma pasta por módulo, com o nome do módulo, e o module-info.java na raiz dela:

$ find src -type f | sort
src/br.com.ana.loja/br/com/ana/loja/Caixa.java
src/br.com.ana.loja/module-info.java
src/br.com.ana.util/br/com/ana/util/interno/Arredonda.java
src/br.com.ana.util/br/com/ana/util/Moeda.java
src/br.com.ana.util/module-info.java

O módulo br.com.ana.util

module br.com.ana.util {
    exports br.com.ana.util;
}
module br.com.ana.util
Declara um módulo e dá o nome dele. O costume é usar o nome do pacote principal (domínio ao contrário). O arquivo se chama sempre module-info.java.
exports br.com.ana.util;
"O pacote br.com.ana.util pode ser usado por outros módulos." O pacote br.com.ana.util.interno não está na lista: fica escondido.
package br.com.ana.util;

import br.com.ana.util.interno.Arredonda;

public class Moeda {
    public static String formatar(double valor) {
        return String.format("R$ %.2f", Arredonda.centavos(valor));
    }
}
package br.com.ana.util.interno;

public class Arredonda {
    public static double centavos(double valor) {
        return Math.round(valor * 100) / 100.0;
    }
}

Dentro do mesmo módulo, a Moeda usa a Arredonda normalmente. O exports só vale para quem está fora.

O módulo br.com.ana.loja

module br.com.ana.loja {
    requires br.com.ana.util;
    requires java.logging;
}
requires br.com.ana.util;
"Este módulo usa o módulo br.com.ana.util." Sem isso, nem os pacotes exportados dele ficam visíveis.
requires java.logging;
O Caixa usa java.util.logging.Logger (veja Depurar), que fica no módulo java.logging do JDK.
(sem requires java.base)
O java.base é exigido automaticamente por todo módulo.
package br.com.ana.loja;

import br.com.ana.util.Moeda;
import java.util.logging.Logger;

public class Caixa {
    private static final Logger LOG = Logger.getLogger("caixa");

    public static void main(String[] args) {
        double total = 0;
        for (String a : args) {
            total += Double.parseDouble(a);
        }
        System.out.println("Itens: " + args.length);
        System.out.println("Total: " + Moeda.formatar(total));
        LOG.info("Venda registrada");
    }
}

Repare: o código das classes é Java comum. Os módulos mexem só na "portaria"; o que acontece dentro das salas não muda.

Missão: o editor do guia não tem módulos, mas o código de dentro deles é o mesmo. Aqui as três classes estão num arquivo só (sem package). Complete o main do caixa: some os preços recebidos nos argumentos e mostre o total com Moeda.formatar, como na classe Caixa acima.

public class Main {
    public static void main(String[] args) {
        double total = 0;
        // some os argumentos aqui

        System.out.println("Itens: " + args.length);
        // mostre "Total: " + o total formatado
    }
}

class Moeda {
    static String formatar(double valor) {
        return String.format("R$ %.2f", Arredonda.centavos(valor));
    }
}

class Arredonda {
    static double centavos(double valor) {
        return Math.round(valor * 100) / 100.0;
    }
}
public class Main {
    public static void main(String[] args) {
        double total = 0;
        for (String a : args) {
            total += Double.parseDouble(a);
        }
        System.out.println("Itens: " + args.length);
        System.out.println("Total: " + Moeda.formatar(total));
    }
}

class Moeda {
    static String formatar(double valor) {
        return String.format("R$ %.2f", Arredonda.centavos(valor));
    }
}

class Arredonda {
    static double centavos(double valor) {
        return Math.round(valor * 100) / 100.0;
    }
}

2. Compilar com --module-path

O module path ("caminho dos módulos") é o equivalente do classpath para módulos: a lista de pastas ou JARs onde o Java procura módulos. Primeiro a Ana compila o util (que não depende de ninguém), depois o loja, apontando o module path para onde o util compilado ficou:

$ javac -d out/br.com.ana.util $(find src/br.com.ana.util -name "*.java")
$ javac -p out -d out/br.com.ana.loja $(find src/br.com.ana.loja -name "*.java")
$ find out -type f | sort
out/br.com.ana.loja/br/com/ana/loja/Caixa.class
out/br.com.ana.loja/module-info.class
out/br.com.ana.util/br/com/ana/util/interno/Arredonda.class
out/br.com.ana.util/br/com/ana/util/Moeda.class
out/br.com.ana.util/module-info.class
$(find src/br.com.ana.util -name "*.java")
Um truque do Bash: o find lista todos os .java do módulo, e a lista vira os argumentos do javac. Inclui o module-info.java.
-d out/br.com.ana.util
Os .class de cada módulo vão para uma pasta com o nome do módulo.
-p out
Abreviação de --module-path out: "procure módulos já compilados na pasta out". É assim que o javac acha o br.com.ana.util.

O module-info.java também é compilado: vira module-info.class. Também dá para compilar todos os módulos de uma vez, com javac -d out --module-source-path src --module br.com.ana.loja,br.com.ana.util.

Rodar com --module

$ java -p out -m br.com.ana.loja/br.com.ana.loja.Caixa 4.50 10 2.25
Itens: 3
Total: R$ 16,75
out. 16, 2026 6:57:14 PM br.com.ana.loja.Caixa main
INFORMAÇÕES: Venda registrada
-p out
--module-path: onde estão os módulos.
-m br.com.ana.loja/br.com.ana.loja.Caixa
--module: o módulo inicial, uma barra, e a classe com o main.
4.50 10 2.25
Os argumentos do programa, como sempre.

JARs modulares

Um JAR com um module-info.class na raiz é um JAR modular. Com --main-class, a classe principal fica anotada no módulo, e basta dar o nome do módulo:

$ mkdir mods
$ jar --create --file mods/util.jar -C out/br.com.ana.util .
$ jar --create --file mods/loja.jar --main-class br.com.ana.loja.Caixa -C out/br.com.ana.loja .
$ java -p mods -m br.com.ana.loja 2.50 2.50
Itens: 2
Total: R$ 5,00
out. 16, 2026 6:58:04 PM br.com.ana.loja.Caixa main
INFORMAÇÕES: Venda registrada
$ jar --describe-module --file mods/loja.jar
br.com.ana.loja jar:file:///home/ana/modulos/mods/loja.jar!/module-info.class
requires br.com.ana.util
requires java.base mandated
requires java.logging
contains br.com.ana.loja
main-class br.com.ana.loja.Caixa

(mandated = "obrigatório": o java.base que todo módulo recebe.) E um detalhe que facilita a vida: um JAR modular também funciona no classpath, como um JAR comum; aí o module-info é simplesmente ignorado:

$ java -cp mods/loja.jar:mods/util.jar br.com.ana.loja.Caixa 3
Itens: 1
Total: R$ 3,00
out. 16, 2026 6:58:04 PM br.com.ana.loja.Caixa main
INFORMAÇÕES: Venda registrada

Tudo o que roda pelo classpath fica num módulo "sem nome" (o unnamed module), que enxerga tudo. É assim que todos os programas das aulas anteriores rodaram, sem que você precisasse saber de módulos.


3. A portaria funcionando: os erros is not visible

Usar um pacote que não foi exportado

A Ana tentou usar a Arredonda direto no Caixa (acrescentou import br.com.ana.util.interno.Arredonda;):

Este código não compila de propósito
$ javac -p out -d out/br.com.ana.loja $(find src/br.com.ana.loja -name "*.java")
src/br.com.ana.loja/br/com/ana/loja/Caixa.java:4: error: package br.com.ana.util.interno is not visible
import br.com.ana.util.interno.Arredonda;
                      ^
  (package br.com.ana.util.interno is declared in module br.com.ana.util, which does not export it)
1 error

A classe Arredonda é public, mas o pacote dela não foi exportado: is not visible ("não é visível"), e a linha de baixo explica: declared in module br.com.ana.util, which does not export it. Isso é o que os módulos acrescentam ao public: público dentro do módulo não é o mesmo que público para o mundo.

Esquecer o requires

Com o module-info.java do loja vazio (module br.com.ana.loja { }, sem nenhum requires):

$ javac -p out -d out/br.com.ana.loja $(find src/br.com.ana.loja -name "*.java")
src/br.com.ana.loja/br/com/ana/loja/Caixa.java:3: error: package br.com.ana.util is not visible
import br.com.ana.util.Moeda;
                 ^
  (package br.com.ana.util is declared in module br.com.ana.util, but module br.com.ana.loja does not read it)
src/br.com.ana.loja/br/com/ana/loja/Caixa.java:4: error: package java.util.logging is not visible
import java.util.logging.Logger;
                ^
  (package java.util.logging is declared in module java.logging, but module br.com.ana.loja does not read it)
2 errors

does not read it = "o módulo br.com.ana.loja não lê (não declarou que usa) aquele módulo". São dois: o br.com.ana.util, da Ana, e o java.logging, do próprio JDK. Conserto: um requires para cada.

Esquecer só o módulo do JDK

Com requires br.com.ana.util;, mas sem o requires java.logging;:

$ javac -p out -d out/br.com.ana.loja $(find src/br.com.ana.loja -name "*.java")
src/br.com.ana.loja/br/com/ana/loja/Caixa.java:4: error: package java.util.logging is not visible
import java.util.logging.Logger;
                ^
  (package java.util.logging is declared in module java.logging, but module br.com.ana.loja does not read it)
1 error

Mesmo erro, agora com um módulo do próprio JDK. Programas no classpath enxergam todos os módulos do JDK; um módulo nomeado só enxerga o java.base e o que declarar. A mensagem já diz qual requires falta: declared in module java.logging.


4. jdeps: de que o código depende?

O jdeps (Java dependencies) analisa classes ou JARs compilados e lista de quais módulos eles dependem:

$ jdeps -s --module-path out out/br.com.ana.loja
br.com.ana.loja -> br.com.ana.util
br.com.ana.loja -> java.base
br.com.ana.loja -> java.logging
$ jdeps -s --module-path out out/br.com.ana.util
br.com.ana.util -> java.base

(-s = summary, resumo.) Ele também funciona com JARs comuns, sem módulos, como os da aula de JAR. É útil para saber quais módulos do JDK um programa antigo usa, antes de usar o jlink:

$ jdeps --print-module-deps ~/jar-loja/caixa.jar ~/jar-loja/util.jar
java.base
$ jdeps -summary ~/jar-loja/caixa.jar
caixa.jar -> java.base
caixa.jar -> not found

O not found aparece porque, analisando o caixa.jar sozinho, o jdeps não achou a Moeda (ela está no util.jar). Passando os dois JARs, a resposta é: só o java.base.


5. jlink: um Java sob medida

Para rodar o caixa, a pessoa precisa de um Java instalado. O JDK inteiro do laboratório ocupa 303 MB, com 69 módulos, compilador, ferramentas… mas o caixa usa só 2 módulos do JDK. O jlink (Java linker, "montador") cria uma pasta com um Java só com os módulos necessários, junto com os seus:

$ jlink --module-path out --add-modules br.com.ana.loja --output caixa-java --strip-debug --no-header-files --no-man-pages --launcher caixa=br.com.ana.loja/br.com.ana.loja.Caixa
$ du -sh caixa-java /opt/jdk-25
56M	caixa-java
303M	/opt/jdk-25
$ ls caixa-java caixa-java/bin
caixa-java:
bin
conf
legal
lib
release

caixa-java/bin:
caixa
java
keytool
$ caixa-java/bin/java --list-modules
br.com.ana.loja
br.com.ana.util
java.base@25.0.4.1
java.logging@25.0.4.1
--module-path out
Onde estão os seus módulos (os do JDK o jlink já conhece).
--add-modules br.com.ana.loja
O módulo raiz. O jlink segue os requires e inclui sozinho o br.com.ana.util, o java.logging e o java.base. Por isso o programa precisa ser modular: o jlink usa os module-info para saber o que levar.
--output caixa-java
A pasta que será criada.
--strip-debug, --no-header-files, --no-man-pages
Deixam de fora informações de depuração, arquivos para programar em C e manuais: tudo o que não faz falta para rodar.
--launcher caixa=módulo/classe
Cria um script bin/caixa que já chama o programa.

De 303 MB para 56 MB, com 4 módulos em vez de 69. Essa pasta pode ser copiada (ou compactada) para outro computador Linux da mesma arquitetura e roda sem Java instalado. Ela é a base do jpackage, citado na aula de JAR. O lançador:

$ cat caixa-java/bin/caixa
#!/bin/sh
JLINK_VM_OPTIONS=
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m br.com.ana.loja/br.com.ana.loja.Caixa "$@"
$ caixa-java/bin/caixa 4.50 10 2.25
Itens: 3
Total: R$ 16.75
Oct 16, 2026 6:57:33 PM br.com.ana.loja.Caixa main
INFORMAÇÕES: Venda registrada

Funcionou, mas olhe com atenção: R$ 16.75 com ponto, e a data em inglês (Oct). Com o JDK completo, era 16,75 e out.. O que sumiu? Os dados de idioma e formato de cada país ficam num módulo separado, o jdk.localedata, que nenhum requires pede, então o jlink não o incluiu. Sem ele, o Java só sabe formatar no padrão inglês. (O INFORMAÇÕES continua em português porque a tradução das mensagens de log mora no próprio java.logging.) Conserto: incluir o módulo à mão, só com os idiomas português (pt-*):

$ jlink --module-path out --add-modules br.com.ana.loja,jdk.localedata --include-locales='pt-*' --output caixa-java-br --strip-debug --no-header-files --no-man-pages --launcher caixa=br.com.ana.loja/br.com.ana.loja.Caixa
$ du -sh caixa-java-br
56M	caixa-java-br
$ caixa-java-br/bin/caixa 4.50 10 2.25
Itens: 3
Total: R$ 16,75
out. 16, 2026 6:57:49 PM br.com.ana.loja.Caixa main
INFORMAÇÕES: Venda registrada

A lição vale para qualquer jlink: o Java sob medida tem só o que foi pedido. Teste o programa com ele antes de entregar.

Missão: reescreva o método interno centavos (o da Arredonda, que o módulo esconde) para arredondar a duas casas: multiplique por 100, arredonde com Math.round e divida por 100.0. A saída deve ser 2.5, 3.14 e 10.0.

public class Main {
    static double centavos(double valor) {
        return valor;
    }

    public static void main(String[] args) {
        System.out.println(centavos(2.499));
        System.out.println(centavos(3.14159));
        System.out.println(centavos(10.0));
    }
}
public class Main {
    static double centavos(double valor) {
        return Math.round(valor * 100) / 100.0;
    }

    public static void main(String[] args) {
        System.out.println(centavos(2.499));
        System.out.println(centavos(3.14159));
        System.out.println(centavos(10.0));
    }
}

6. Quando vale a pena usar módulos?

Com franqueza: a maioria dos projetos Java não usa módulos. Aplicações web, sistemas de empresas e projetos com Maven ou Gradle continuam no classpath, e funcionam muito bem. Muitas bibliotecas populares ainda não trazem module-info, e misturar as duas coisas exige cuidado. Você não precisa de módulos para escrever bons programas.

SituaçãoMódulos?
Aprendendo Java, programas pequenos, exercíciosNão
Aplicação comum com Maven/Gradle e várias bibliotecasGeralmente não
Você escreve uma biblioteca para outras pessoas e quer esconder os pacotes internosSim, vale a pena
Você quer entregar um programa com um Java enxuto (jlink/jpackage), por exemplo um aplicativo de desktopSim
Um sistema grande, com equipes diferentes, em que as fronteiras entre as partes precisam ser respeitadasPode valer

Mesmo sem escrever module-info, é bom saber que os módulos existem: eles explicam mensagens como is not visible, o java.base que aparece nos stack traces (at java.base/java.util…) e a organização da documentação oficial, que é dividida por módulo.


Quando dá errado

package … is not visible

O que aconteceu: leia a linha entre parênteses. which does not export it: o pacote não está nos exports do outro módulo (ele é interno; use outra classe, ou exporte o pacote se o módulo for seu). does not read it: falta o requires no seu module-info.java.

No matching locales found for "pt-BR"
$ jlink --module-path out --add-modules br.com.ana.loja,jdk.localedata --include-locales=pt-BR --output caixa-java-br --strip-debug --no-header-files --no-man-pages --launcher caixa=br.com.ana.loja/br.com.ana.loja.Caixa
Error: No matching locales found for "pt-BR". Check the specified pattern.

O que aconteceu: neste JDK, o jlink não achou o padrão pt-BR escrito assim. Como resolver: use --include-locales='pt-*' (todo o português), como na seção 5.

Números e datas em inglês no Java do jlink

O que aconteceu: faltou o módulo jdk.localedata. Como resolver: --add-modules …,jdk.localedata (de preferência com --include-locales para não levar todos os idiomas). Use caixa-java/bin/java --list-modules para conferir o que foi incluído.

Module … not found ao rodar
$ java -p mods/loja.jar -m br.com.ana.loja 1
Error occurred during initialization of boot layer
java.lang.module.FindException: Module br.com.ana.util not found, required by br.com.ana.loja

O que aconteceu: o module path (-p) tem só o loja.jar; o br.com.ana.util, que ele exige, não está lá. Ao contrário do classpath, o Java confere antes de começar (during initialization) e se recusa a rodar sem todos os requires. Como resolver: aponte o -p para a pasta com todos os módulos (-p mods).


Referência

ItemO que faz
module nome { … }Declara o módulo (module-info.java, na raiz dos fontes)
requires modulo;Este módulo usa aquele
exports pacote;Este pacote pode ser usado por outros módulos
javac -p out -d out/mod $(find src/mod -name "*.java")Compila um módulo usando outros já compilados
javac -d out --module-source-path src --module a,bCompila vários módulos de uma vez
java -p out -m modulo/pacote.ClasseRoda pelo module path
jar --create --file x.jar --main-class … -C pasta .Cria um JAR modular
java --list-modules / --describe-module mLista os módulos / mostra um
jdeps -s … / jdeps --print-module-deps …Dependências de classes ou JARs
jlink --module-path … --add-modules … --output …Cria um Java sob medida

Exercícios

1. Exportar um pacote

Complete o module-info.java do módulo br.com.ana.agenda para que outros módulos usem o pacote br.com.ana.agenda.modelo:

module br.com.ana.agenda {
    
}

2. Usar um módulo do JDK

O seu módulo usa java.sql.Connection, do módulo java.sql. Complete:

3. Rodar

Complete para rodar a classe br.com.ana.agenda.App do módulo br.com.ana.agenda, com os módulos na pasta mods:

java -p mods -m

4. A classe Arredonda é public, mas o pacote dela não está nos exports. Outro módulo consegue usá-la?

5. Para que serve o jlink?

6. Qual módulo todo módulo recebe sem precisar de requires?


Resumo

  • Um módulo agrupa pacotes e declara, no module-info.java, o que usa (requires) e o que mostra (exports). O java.base vem sempre.
  • Pacote não exportado fica invisível fora do módulo, mesmo com classes public.
  • Compilar e rodar: javac -p … -d … e java -p … -m modulo/classe.
  • Erros is not visible: does not export it (falta exports no outro) ou does not read it (falta requires no seu).
  • jdeps mostra as dependências; jlink cria um Java sob medida (aqui, 56 MB contra 303 MB). Inclua jdk.localedata se precisar de formatos do Brasil.
  • O classpath continua funcionando; a maioria dos projetos não usa módulos. Eles valem a pena em bibliotecas e em programas entregues com jlink.