Construção Automatizada — Maven e Gradle

Construção Automatizada — Maven e Gradle

Como Maven e Gradle baixam dependências, compilam, testam e empacotam um projeto Java com um comando: coordenadas no Maven Central, pom.xml, build.gradle.kts, fases e tarefas, escopos, conflitos de versões transitivas, wrapper e os critérios para escolher entre as duas ferramentas.
Java

• • 17 min de leitura

As ferramentas que baixam as dependências, compilam, testam e empacotam um projeto Java com um único comando, e como escolher entre as duas mais usadas.

"Do Arquivo Solto ao Projeto" terminou no inferno do classpath: com dezenas de bibliotecas, cada uma com as suas dependências, montar à mão a compilação, o classpath e o empacotamento se torna impraticável. Toda linguagem moderna tem ferramentas para isso. O ecossistema Java tem duas dominantes, o Maven e o Gradle. Nesta aula você vai conhecer o que elas resolvem, converter o projeto de tarefas para as duas, entender como as dependências são declaradas e resolvidas, e ver os critérios para escolher entre elas.

O que uma ferramenta de build faz

Uma ferramenta de build (construção) automatiza quatro coisas:

  1. Dependências. Você declara quais bibliotecas o projeto usa, com nome e versão. A ferramenta baixa cada uma, e as dependências delas, de um repositório central, guarda uma cópia local e monta o classpath.
  2. Estrutura padrão. O lugar dos fontes, dos testes e dos recursos é fixo por convenção, e qualquer desenvolvedor Java reconhece um projeto à primeira vista.
  3. Ciclo de construção. Compilar, rodar os testes, empacotar e publicar viram comandos, sempre executados na mesma ordem.
  4. Reprodutibilidade. O mesmo arquivo de configuração produz o mesmo resultado na máquina de qualquer desenvolvedor e no servidor que constrói o projeto automaticamente.

Maven Central e as coordenadas

As bibliotecas Java de código aberto são publicadas num repositório público, o Maven Central (https://central.sonatype.com/), com centenas de milhares de bibliotecas. Tanto o Maven quanto o Gradle usam esse repositório. Cada biblioteca é identificada por três coordenadas:

  • groupId: a organização, no formato de domínio invertido dos pacotes, como org.apache.commons.
  • artifactId: o nome do projeto, como commons-lang3.
  • version: a versão, como 3.18.0.

Juntas, formam uma notação que você vai ver em toda documentação de biblioteca: org.apache.commons:commons-lang3:3.18.0.

A maioria das bibliotecas segue o versionamento semântico (SemVer), MAIOR.MENOR.CORREÇÃO. Uma correção (3.18.0 para 3.18.1) só corrige defeitos. Uma versão menor (3.18 para 3.19) acrescenta recursos sem quebrar o código existente. Uma versão maior (3 para 4) pode quebrar compatibilidade. Isso ajuda a avaliar o risco de uma atualização.

Maven

O Maven existe desde 2004 e é o mais usado no mundo corporativo. Tudo é configurado num arquivo XML na raiz do projeto, o pom.xml (Project Object Model). A estrutura de pastas é fixa por convenção:

tarefas/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/          ← o código (os pacotes br/com/exemplo/...)
    │   └── resources/     ← arquivos de configuração, textos, imagens
    └── test/
        ├── java/          ← os testes
        └── resources/

Os fontes do projeto de tarefas vão para src/main/java, com os mesmos pacotes. O pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>br.com.exemplo</groupId>
    <artifactId>tarefas</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.18.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>br.com.exemplo.tarefas.app.Principal</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

O projeto tem as suas próprias coordenadas (groupId, artifactId e version), e poderia ser publicado e usado por outros. A propriedade maven.compiler.release define a versão do Java para a qual o código é compilado, e garante que ele só use recursos da biblioteca padrão dessa versão. A dependência declara a biblioteca Apache Commons Lang, com utilitários de texto como StringUtils.abbreviate, que encurta textos longos. O plugin configura o manifesto do JAR, com a Main-Class que "Do Arquivo Solto ao Projeto" montou à mão. As versões dos exemplos eram as vigentes quando este artigo foi escrito. Antes de usar, confira as atuais no Maven Central.

Com a dependência declarada, o código pode usá-la como qualquer classe:

import org.apache.commons.lang3.StringUtils;
// ...
IO.println("  #" + t.id() + " " + StringUtils.abbreviate(t.titulo(), 20));

O ciclo de vida

O Maven organiza a construção em fases, sempre executadas em ordem:

Fase Faz
validate verifica se o projeto está bem configurado
compile compila o código de src/main/java
test compila e roda os testes de src/test/java
package empacota o resultado, geralmente num JAR, em target/
verify executa verificações adicionais, como testes de integração
install copia o JAR para o repositório local da máquina
deploy publica o JAR num repositório remoto

Pedir uma fase executa todas as anteriores. mvn package valida, compila, testa e empacota, e o JAR aparece em target/tarefas-1.0.0.jar. Se um teste falhar, o empacotamento não acontece. É uma das garantias mais valiosas da ferramenta. mvn clean apaga a pasta target, e mvn clean package reconstrói tudo do zero.

Cada fase é executada por plugins. O maven-compiler-plugin compila, o maven-surefire-plugin roda os testes e o maven-jar-plugin empacota. O Maven em si é pequeno: quase tudo o que ele faz vem de plugins, baixados do Maven Central como qualquer dependência.

Na primeira execução, o Maven baixa os plugins e as dependências para o repositório local, a pasta ~/.m2/repository. Nas seguintes, reaproveita o que já está lá. Um JAR baixado uma vez serve a todos os projetos da máquina.

Gradle

O Gradle surgiu em 2008 como alternativa ao Maven. É a ferramenta oficial do desenvolvimento Android, e usada por grandes projetos como o próprio Spring Framework. A configuração é escrita em código, numa linguagem própria baseada em Kotlin (ou, nos projetos mais antigos, em Groovy). O projeto de tarefas, com a mesma estrutura de pastas do Maven, tem dois arquivos. settings.gradle.kts:

rootProject.name = "tarefas"

E build.gradle.kts:

plugins {
    application
}

group = "br.com.exemplo"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.apache.commons:commons-lang3:3.18.0")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

application {
    mainClass = "br.com.exemplo.tarefas.app.Principal"
}

tasks.jar {
    manifest {
        attributes("Main-Class" to application.mainClass)
    }
}

Comparando com o pom.xml: o plugin application acrescenta ao projeto a capacidade de ser executado e distribuído como aplicação. A dependência usa a notação curta das coordenadas. O bloco toolchain declara que o projeto usa o Java 25, e o Gradle procura, ou baixa, um JDK dessa versão para compilar, independente do Java com que a própria ferramenta está rodando. O suporte ao Java 25 exige o Gradle 9.1 ou mais novo.

O Gradle trabalha com tarefas em vez de fases:

gradle build     # compila, testa e empacota: build/libs/tarefas-1.0.0.jar
gradle run       # compila e executa a classe principal
gradle test      # só os testes
gradle clean     # apaga a pasta build

As tarefas dependem umas das outras, como as fases do Maven: build depende de test, que depende de compileJava. A diferença é que, no Gradle, esse grafo de tarefas é livre, e cada plugin acrescenta tarefas e dependências novas. Com o plugin application, gradle build também gera em build/distributions um arquivo .zip e um .tar com o JAR, as dependências e scripts de execução para Linux e Windows, prontos para entregar.

A principal vantagem técnica do Gradle é o build incremental. Ele registra as entradas e saídas de cada tarefa e pula as que não mudaram desde a última execução, e pode reaproveitar resultados de um cache compartilhado entre máquinas. Em projetos grandes, isso reduz uma construção de vários minutos a segundos.

Escopos de dependência

Nem toda dependência é necessária o tempo todo. Uma biblioteca de testes só é usada nos testes. Um driver de banco de dados só é necessário na execução, não na compilação. As duas ferramentas permitem declarar isso:

Situação Maven (<scope>) Gradle
código principal, compilação e execução compile (padrão) implementation
só na execução runtime runtimeOnly
fornecida pelo ambiente de execução provided compileOnly
só nos testes test testImplementation

No Maven, o escopo é um elemento dentro de <dependency>: <scope>test</scope>. No Gradle, é a própria palavra usada na declaração: testImplementation("..."). Declarar o escopo certo mantém o JAR final enxuto e evita que o código principal use, por engano, uma biblioteca que só deveria existir nos testes.

Dependências transitivas e conflitos

Quando você declara uma dependência, a ferramenta também baixa as dependências dela, e as dependências dessas, e assim por diante. São as dependências transitivas, e elas resolvem a primeira metade do inferno do classpath: você declara o que usa diretamente, e a ferramenta descobre o resto. Para ver a árvore completa:

mvn dependency:tree
gradle dependencies

A outra metade do problema continua existindo: e se duas bibliotecas dependerem de versões diferentes de uma terceira? O classpath só pode ter uma. Cada ferramenta tem uma regra:

  • O Maven escolhe a versão mais próxima da raiz na árvore de dependências. Em caso de empate de distância, fica a declarada primeiro.
  • O Gradle escolhe a versão mais alta entre as pedidas.

Nenhuma regra é segura em todos os casos. Se a versão escolhida não for compatível com uma das bibliotecas que pediram outra, o resultado é um erro durante a execução, frequentemente um NoSuchMethodError: o código foi compilado contra um método que não existe na versão que acabou no classpath. Quando isso acontece, a árvore de dependências mostra o conflito, e a solução é fixar explicitamente a versão, com a seção <dependencyManagement> no Maven ou com restrições (constraints) no Gradle.

Grandes frameworks publicam um BOM (Bill of Materials), uma lista de versões testadas juntas, que o projeto importa para alinhar todas as dependências relacionadas de uma vez. O Spring Boot, na trilha de web, funciona assim, e é uma das razões de sua popularidade.

Dependências também trazem riscos de segurança: uma vulnerabilidade numa biblioteca transitiva, que você nem sabe que usa, afeta o seu programa. O caso mais famoso foi o Log4Shell, em dezembro de 2021, uma falha grave na biblioteca de log Log4j, presente de forma transitiva em milhares de sistemas. Ferramentas que verificam as dependências contra bases de vulnerabilidades conhecidas, como o OWASP Dependency-Check e os alertas dos serviços de hospedagem de código, fazem parte de um build profissional.

O wrapper

Maven e Gradle precisam estar instalados para funcionar, e versões diferentes da ferramenta podem produzir resultados diferentes. O wrapper resolve isso: um pequeno script, incluído no próprio projeto, que baixa e usa a versão certa da ferramenta. No Maven, mvn wrapper:wrapper cria os scripts mvnw e mvnw.cmd. No Gradle, gradle wrapper cria gradlew e gradlew.bat. A partir daí, todos usam o wrapper:

./mvnw package        # no Windows: mvnw.cmd package
./gradlew build       # no Windows: gradlew.bat build

Qualquer pessoa que clonar o projeto consegue construí-lo tendo apenas um JDK instalado, com a versão exata da ferramenta que o projeto definiu. O wrapper vai para o controle de versão. As pastas target/ e build/, e o repositório local ~/.m2, nunca vão, porque são geradas.

Qual escolher

As duas ferramentas resolvem os mesmos problemas, usam o mesmo repositório e a mesma estrutura de pastas. As diferenças:

Maven Gradle
Configuração XML declarativo, sempre no mesmo formato código (Kotlin ou Groovy), flexível
Curva de aprendizado menor: o formato é rígido e previsível maior: a flexibilidade permite estruturas muito diferentes
Desempenho bom melhor em projetos grandes, com build incremental e cache
Onde predomina sistemas corporativos, a maioria dos projetos Spring Android, projetos grandes com muitos módulos

A rigidez do Maven é ao mesmo tempo o defeito e a virtude dele: todo pom.xml tem o mesmo formato, e personalizações exigem plugins. A flexibilidade do Gradle permite qualquer personalização, e também permite que cada projeto invente a sua própria estrutura, difícil de entender para quem chega.

Neste curso, os projetos da trilha de web usam Maven, o formato mais comum na documentação e nos projetos Spring, e os da trilha de jogos usam Gradle, porque é o que o gerador oficial de projetos libGDX produz. Sabendo as duas, você lê qualquer projeto Java.

O projeto de tarefas agora tem uma estrutura profissional, dependências declaradas e um build reproduzível. Mas ele continua com uma fragilidade herdada do classpath: qualquer classe pública de qualquer pacote pode ser usada por qualquer outra, e pacotes que eram detalhes internos acabam sendo usados por quem não deveria. A ferramenta de build organiza o projeto, mas não protege as fronteiras entre as partes do código. Declarar explicitamente o que cada parte oferece ao mundo e de que partes ela depende é o papel do sistema de módulos do Java.

Fontes e leituras recomendadas

Exercícios

Exercício 1

As coordenadas com.google.code.gson:gson:2.13.1 identificam uma biblioteca. Diga o que é cada parte e, segundo o versionamento semântico, qual é o risco de atualizar para 2.13.2, para 2.14.0 e para 3.0.0.

Ver resposta

✓ Resposta: com.google.code.gson é o groupId, a organização, gson é o artifactId, o nome do projeto, e 2.13.1 é a versão. Para 2.13.2, uma versão de correção, o risco é baixo: só defeitos corrigidos, e a atualização deve ser feita sem hesitar, especialmente se corrigir uma vulnerabilidade. Para 2.14.0, uma versão menor, o risco é moderado: há recursos novos, mas o código existente deve continuar compilando e funcionando. Para 3.0.0, uma versão maior, o risco é alto: podem ter sido removidos ou alterados métodos que o seu código usa, e é preciso ler as notas de lançamento e testar. Uma ressalva importante: o versionamento semântico é uma promessa do autor da biblioteca, e não uma garantia técnica. Algumas bibliotecas quebram compatibilidade em versões menores, e é por isso que os testes automatizados rodam a cada atualização.

Exercício 2

Para cada dependência, escolha o escopo no Maven e a configuração no Gradle: (a) uma biblioteca de JSON usada pelo código principal; (b) o JUnit; (c) o driver de um banco de dados, que o código acessa só por interfaces padronizadas; (d) a API de servlets, fornecida pelo servidor de aplicação em que o programa vai rodar.

Ver resposta

✓ Resposta: (a) Maven compile (o padrão, sem declarar escopo) e Gradle implementation: o código principal usa as classes dela na compilação e na execução. (b) Maven test e Gradle testImplementation: só os testes a usam, e ela não deve ir para o JAR final nem ficar disponível ao código principal. (c) Maven runtime e Gradle runtimeOnly: o código compila usando apenas as interfaces padronizadas de acesso a banco, que você vai ver em "Dados que Sobrevivem", e o driver concreto só precisa existir na execução. Isso também impede que o código dependa, por engano, de classes específicas daquele banco. (d) Maven provided e Gradle compileOnly: o código precisa da API para compilar, mas o servidor já a fornece na execução, e incluí-la de novo causaria conflitos de versão.

Exercício 3

O que mvn package executa? Um colega costuma rodar mvn package -DskipTests "para ir mais rápido". Qual é o risco?

Ver resposta

✓ Resposta: mvn package executa todas as fases até package, na ordem: validate, compile, test e package. O resultado é um JAR em target/, gerado só se todos os testes passarem. A opção -DskipTests pula a execução dos testes, e o JAR é gerado mesmo que o código esteja quebrado. O risco é entregar um artefato que nunca foi verificado: um defeito que os testes pegariam em segundos chega a produção. Se isso vira hábito, os testes deixam de ser executados de verdade, começam a falhar sem que ninguém perceba, e em pouco tempo ninguém mais confia neles. Pular os testes pode ser razoável numa situação pontual e consciente, como gerar um JAR para investigar outro problema, mas nunca num build que produz algo para ser entregue. Se os testes são lentos a ponto de incomodar, o problema a resolver é a lentidão dos testes.

Exercício 4

O projeto usa as bibliotecas A e B. A depende de util:1.2 e B depende de util:2.0. Diga que versão de util o Maven e o Gradle escolhem, que problema pode aparecer, e como investigar e resolver.

Ver resposta

✓ Resposta: Na árvore, as duas versões estão à mesma distância da raiz (segundo nível). O Maven, nesse empate, escolhe a que aparece primeiro na ordem de declaração: se A foi declarada antes de B, fica util:1.2. O Gradle escolhe a mais alta, util:2.0. Qualquer que seja a escolha, uma das bibliotecas vai rodar com uma versão de util diferente da que esperava. Se a 2.0 removeu ou mudou métodos que A usa, ou se a 1.2 não tem métodos que B precisa, o programa compila normalmente e falha na execução com NoSuchMethodError ou NoClassDefFoundError, no momento em que o código afetado é executado. Para investigar, mvn dependency:tree ou gradle dependencies mostram as duas versões pedidas e qual foi escolhida. Para resolver, primeiro verifique se existe uma versão de A compatível com util:2.0, e atualize A. Se não existir, fixe a versão de util com dependencyManagement ou constraints, e rode os testes para confirmar que as duas bibliotecas funcionam com ela. Em casos extremos, existem técnicas para isolar versões, como renomear os pacotes da dependência dentro de um JAR, mas são último recurso.

Exercício 5

Liste o que deve e o que não deve ir para o controle de versão num projeto Maven com wrapper, e justifique.

Ver resposta

✓ Resposta: Devem ir: o código-fonte (src/), o pom.xml, os scripts do wrapper (mvnw, mvnw.cmd) e a pasta .mvn/wrapper, que guarda a configuração da versão do Maven. Com isso, qualquer pessoa, ou servidor, reconstrói o projeto a partir do repositório tendo só um JDK. Não devem ir: a pasta target/, porque tudo nela é gerado pelo build e pode ser recriado; o repositório local ~/.m2, que fica fora do projeto e é da máquina, e não do projeto; e arquivos de configuração pessoal de IDEs, como .idea/ ou .vscode/, que variam de pessoa para pessoa. Esses itens vão num arquivo .gitignore. A regra geral: versione o que é fonte de alguma coisa, e não o que é gerado a partir dela. Arquivos gerados no controle de versão causam conflitos a cada build, incham o repositório e podem ficar desatualizados em relação aos fontes.

Comentários

Mais em Java

Extensão — Um App Desktop que Consome a API (MVVM)
Extensão — Um App Desktop que Consome a API (MVVM)

Nesta aula a janela de catálogo passa a falar com a API REST das Trilhas da…

Agrupar, Achatar, Reduzir — Collectors e Gatherers
Agrupar, Achatar, Reduzir — Collectors e Gatherers

Como reunir os elementos de uma stream Java em mapas agrupados, partições e…

Da Primeira Classe à JVM: o Mapa da Jornada em Java
Da Primeira Classe à JVM: o Mapa da Jornada em Java

O mapa do curso de Java: setenta e oito aulas em nove fases e três trilhas, do…