Dependências Maven no Java
Declare e gerencie dependências Java no Maven com dependency, scope e resolução transitiva.
Projetos Java reais raramente existem sozinhos. Eles utilizam frameworks de logging, clientes HTTP, parsers JSON e bibliotecas de testes escritas por outras pessoas. O papel do Maven é buscar essas bibliotecas, buscar as dependências delas por sua vez, e montar um único classpath consistente sem que você precise rastrear um único JAR manualmente. Entender como ele faz isso é a diferença entre uma build que simplesmente funciona e uma tarde perdida com um NoSuchMethodError.
Este capítulo aborda como uma dependência é identificada (coordenadas), como os escopos controlam onde cada biblioteca é visível, como o Maven percorre o grafo de dependências transitivas e como ele resolve conflitos de versão. Pressupõe que você já tem um pom.xml; caso contrário, comece pelo capítulo Maven POM.
Coordenadas: Como uma Dependência É Identificada
Todo artefato no mundo Maven é identificado por um conjunto de coordenadas. As três que você sempre fornece são o groupId (quem o publica, geralmente um domínio invertido), o artifactId (o nome do projeto) e a version. Juntas, elas apontam para exatamente um JAR em um repositório.
Você declara uma dependência dentro do bloco <dependencies> do seu pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>A forma abreviada groupId:artifactId:version é chamada de string GAV, e você a verá em todo lugar: em mensagens de erro, na árvore de dependências e nas páginas do repositório central. Uma quarta coordenada, o type (jar por padrão), e uma quinta, o classifier (para variantes como sources ou javadoc), completam o endereço completo.
Escopos: Quando uma Dependência É Visível
Nem toda dependência pertence a todos os classpaths. Um framework de testes não deve ser incluído no seu JAR de produção, e uma API de servlet fornecida pelo servidor de aplicação não deve ser empacotada duas vezes. O Maven controla isso com o elemento <scope>.
| Escopo | Compilação | Teste | Execução | Empacotado | Uso típico |
|---|---|---|---|---|---|
compile (padrão) | Sim | Sim | Sim | Sim | Bibliotecas principais chamadas diretamente |
provided | Sim | Sim | Não | Não | APIs fornecidas pelo contêiner (servlet, driver JDBC) |
runtime | Não | Sim | Sim | Sim | Implementações necessárias apenas em tempo de execução |
test | Não | Sim | Não | Não | JUnit, Mockito, bibliotecas de asserção |
system | Sim | Sim | Não | Não | JARs locais por caminho absoluto (evitar) |
Uma dependência com escopo test é a mais comum fora do padrão. O JUnit nunca vazará para o seu artefato distribuído:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Dependências Transitivas
Quando você depende de uma biblioteca, também depende de tudo aquilo de que ela depende. O Maven lê o pom.xml publicado de cada artefato, segue essas declarações recursivamente e adiciona o grafo inteiro ao seu classpath automaticamente. Essas entradas indiretas são dependências transitivas.
É por isso que uma única linha <dependency> para um framework web pode trazer dezenas de JARs que você nunca nomeou. O escopo ainda se aplica durante essa varredura: uma dependência com escopo test não puxa suas dependências transitivas para o seu classpath de compilação, e dependências provided não são propagadas transitivamente.
Você pode ver o grafo completo com o plugin de dependências:
$ mvn dependency:tree
[INFO] com.example:app:jar:1.0
[INFO] +- org.web:server:jar:2.4:compile
[INFO] | +- org.log:log:jar:1.2:compile
[INFO] | \- org.json:json:jar:1.7:compile - omitted for conflict with 1.9
[INFO] \- org.json:json:jar:1.9:compileMediação de Conflito de Versão
Um grafo tão profundo quase sempre requer o mesmo artefato em duas versões diferentes. O Maven não pode colocar ambas em um único classpath, então escolhe uma usando a mediação nearest-wins (o mais próximo vence): a versão declarada na profundidade mais rasa a partir da raiz do projeto prevalece, e as demais são omitidas por conflito.
Na árvore acima, seu projeto solicita org.json:json:1.9 diretamente (profundidade 1), enquanto org.web:server solicita 1.7 transitivamente (profundidade 2). A declaração na profundidade 1 vence. Se dois candidatos estiverem na mesma profundidade, o declarado primeiro no pom.xml vence.
Quando a escolha automática está errada, você assume o controle explicitamente. Uma dependência direta sempre vence, ou você pode fixar versões em todo o projeto com <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Para eliminar completamente um ramo transitivo indesejado, use <exclusions>:
<dependency>
<groupId>org.web</groupId>
<artifactId>server</artifactId>
<version>2.4</version>
<exclusions>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
</exclusions>
</dependency>Um Exemplo Completo
O Maven em si não está disponível neste executor de código, então o programa abaixo modela seu resolvedor em Java puro. Ele publica alguns artefatos em um pequeno repositório em memória e, em seguida, realiza a mesma varredura em largura e mediação nearest-wins que o Maven usa para aplainar um grafo de dependências em um único classpath. Observe como o org.json:json:1.7 mais profundo perde para o 1.9 mais raso.
O que observar na execução:
- O classpath resolvido lista cada artefato exatamente uma vez, espelhando como o Maven achata um grafo em um único conjunto de JARs sem entradas duplicadas de group:artifact.
org.json:jsonaparece na versão1.9, não1.7, porque a mediação nearest-wins mantém o candidato encontrado na profundidade mais rasa (profundidade 1 vence sobre profundidade 2).- A coluna
depthtorna "mais próximo" concreto:app:appestá na profundidade 0, suas dependências diretas estão na profundidade 1, eorg.log:logpuxado transitivamente está na profundidade 2. - "Tree edges visited: 4" conta os relacionamentos de dependência declarados, enquanto "Distinct artifacts: 4" mostra o grafo reduzido a quatro coordenadas únicas após a mediação.
- Uma coordenada é ignorada no momento em que é vista novamente (
depthOf.containsKey(ga)), o que é exatamente o motivo pelo qual o1.7mais profundo é "omitido por conflito" em vez de ser adicionado uma segunda vez.
Uma vez que suas dependências se resolvam corretamente, a próxima pergunta é quando o Maven baixa, compila, testa e empacota. Essa ordenação é governada pelas fases de build abordadas no capítulo ciclo de vida do Maven.