Aprendizado por comparação com o que já sabe. Recentemente fui surpreendido ao assumir que o Rust funcionava como o Java em relação à resolução de versão de dependência transitiva. Neste post, quero comparar os dois.Aprendizado por comparação com o que já sabe. Recentemente fui surpreendido ao assumir que o Rust funcionava como o Java em relação à resolução de versão de dependência transitiva. Neste post, quero comparar os dois.

Resolução de Versão de Dependência Transitiva em Rust e Java: Comparando os Dois

2025/09/21 23:00

Você aprende comparando com o que já sabe. Recentemente fui surpreendido ao assumir que o Rust funcionava como o Java em relação à resolução de versões de dependências transitivas. Neste post, quero comparar os dois.

Dependências, Transitividade e Resolução de Versões

Antes de mergulhar nas especificidades de cada stack, vamos descrever o domínio e os problemas que vêm com ele.

\ Ao desenvolver qualquer projeto acima do nível Hello World, é provável que enfrente problemas que outros já enfrentaram antes. Se o problema for generalizado, a probabilidade é alta de que alguém tenha sido gentil e cívico o suficiente para ter empacotado o código que o resolve, para que outros possam reutilizá-lo. Agora, pode usar o pacote e concentrar-se em resolver o seu problema principal. É assim que a indústria constrói a maioria dos projetos hoje, mesmo que traga outros problemas: você se apoia nos ombros de gigantes.

\ As linguagens vêm com ferramentas de compilação que podem adicionar esses pacotes ao seu projeto. A maioria delas refere-se aos pacotes que adiciona ao seu projeto como dependências. Por sua vez, as dependências dos projetos podem ter as suas próprias dependências: estas últimas são chamadas dependências transitivas.

Transitive dependencies

No diagrama acima, C e D são dependências transitivas.

\ As dependências transitivas têm problemas próprios. O maior deles é quando uma dependência transitiva é necessária a partir de diferentes caminhos, mas em diferentes versões. No diagrama abaixo, A e B dependem ambos de C, mas em versões diferentes.

Qual versão de C a ferramenta de compilação deve incluir no seu projeto? Java e Rust têm respostas diferentes. Vamos descrevê-las por sua vez.

Resolução de Versão de Dependência Transitiva em Java

Lembrete: o código Java compila para bytecode, que é então interpretado em tempo de execução (e às vezes compilado para código nativo, mas isso está fora do nosso espaço de problema atual). Primeiro descreverei a resolução de dependência em tempo de execução e a resolução de dependência em tempo de compilação.

\ Em tempo de execução, a Máquina Virtual Java oferece o conceito de classpath. Quando precisa carregar uma classe, o runtime pesquisa através do classpath configurado em ordem. Imagine a seguinte classe:

public static Main {     public static void main(String[] args) {         Class.forName("ch.frankel.Dep");     } } 

\ Vamos compilá-la e executá-la:

java -cp ./foo.jar:./bar.jar Main 

\ O código acima primeiro procurará no foo.jar pela classe ch.frankel.Dep. Se encontrada, para aí e carrega a classe, independentemente de também poder estar presente no bar.jar; se não, procura mais adiante na classe bar.jar. Se ainda não for encontrada, falha com um ClassNotFoundException.

\ O mecanismo de resolução de dependência em tempo de execução do Java é ordenado e tem uma granularidade por classe. Aplica-se quer execute uma classe Java e defina o classpath na linha de comando como acima, quer execute um JAR que define o classpath no seu manifesto.

\ Vamos alterar o código acima para o seguinte:

public static Main {     public static void main(String[] args) {         var dep = new ch.frankel.Dep();     } } 

\ Como o novo código referencia Dep diretamente, o novo código requer resolução de classe em tempo de compilação. A resolução do classpath funciona da mesma forma:

javac -cp ./foo.jar:./bar.jar Main 

\ O compilador procura por Dep em foo.jar, depois em bar.jar se não for encontrado. O acima é o que aprende no início da sua jornada de aprendizado de Java.

\ Depois, a sua unidade de trabalho é o Java Archive, conhecido como JAR, em vez da classe. Um JAR é um arquivo ZIP glorificado, com um manifesto interno que especifica a sua versão.

\ Agora, imagine que é um usuário de foo.jar. Os desenvolvedores de foo.jar definiram um classpath específico ao compilar, possivelmente incluindo outros JARs. Precisará desta informação para executar o seu próprio comando. Como é que um desenvolvedor de biblioteca passa este conhecimento para os usuários downstream?

\ A comunidade surgiu com algumas ideias para responder a esta pergunta: A primeira resposta que pegou foi o Maven. O Maven tem o conceito de Project Object Model, onde define os metadados do seu projeto, bem como as dependências. O Maven pode facilmente resolver dependências transitivas porque elas também publicam o seu POM, com as suas próprias dependências. Assim, o Maven pode rastrear as dependências de cada dependência até às dependências folha.

\ Agora, voltando à declaração do problema: como é que o Maven resolve conflitos de versão? Qual versão de dependência o Maven resolverá para C, 1.0 ou 2.0?

\ A documentação é clara: a mais próxima.

Dependency resolution with the same dependency in different versions

No diagrama acima, o caminho para v1 tem uma distância de dois, um para B, depois um para C; enquanto isso, o caminho para v2 tem uma distância de três, um para A, depois um para D, e finalmente um para C. Assim, o caminho mais curto aponta para v1.

\ No entanto, no diagrama inicial, ambas as versões de C estão à mesma distância do artefato raiz. A documentação não fornece resposta. Se estiver interessado nisso, depende da ordem de declaração de A e B no POM! Em resumo, o Maven retorna uma única versão de uma dependência duplicada para incluí-la no classpath de compilação.

\ Se A pode trabalhar com C v2.0 ou B com C 1.0, ótimo! Caso contrário, provavelmente precisará atualizar a sua versão de A ou fazer downgrade da sua versão de B, para que a versão C resolvida funcione com ambos. É um processo manual que é doloroso - pergunte-me como sei. Pior, pode descobrir que não há versão C que funcione com A e B. Hora de substituir A ou B.

Resolução de Versão de Dependência Transitiva em Rust

Rust difere de Java em vários aspectos, mas penso que os seguintes são os mais relevantes para a nossa discussão:

  • Rust tem a mesma árvore de dependências em tempo de compilação e em tempo de execução
  • Fornece uma ferramenta de compilação pronta a usar, Cargo
  • As dependências são resolvidas a partir da fonte

\ Vamos examiná-los um por um.

\ Java compila para bytecode, depois executa este último. Precisa definir o classpath tanto no momento da compilação como no momento da execução. Compilar com um classpath específico e executar com um diferente pode levar a erros. Por exemplo, imagine que compila com uma classe da qual depende, mas a classe está ausente em tempo de execução. Ou alternativamente, está presente, mas numa versão incompatível.

\ Contrariamente a esta abordagem modular, Rust compila para um pacote nativo único o código da crate e cada dependência. Além disso, Rust também fornece a sua própria compilação, evitando assim ter de lembrar as peculiaridades de diferentes ferramentas. Mencionei o Maven, mas outras ferramentas de compilação provavelmente têm regras diferentes para resolver a versão no caso de uso acima.

\ Finalmente, Java resolve dependências a partir de binários: JARs. Pelo contrário, Rust resolve dependências a partir de fontes. No momento da compilação, o Cargo resolve toda a árvore de dependências, baixa todas as fontes necessárias e compila-as na ordem correta.

\ Com isto em mente, como é que o Rust resolve a versão da dependência C no problema inicial? A resposta pode parecer estranha se vier de um background Java, mas Rust inclui ambos. De facto, no diagrama acima, Rust compilará A com C v1.0 e compilará B com C v2.0. Problema resolvido.

Conclusão

As linguagens JVM, e Java em particular, oferecem tanto um classpath em tempo de compilação como um classpath em tempo de execução. Permite modularidade e reutilização, mas abre a porta a problemas relacionados com a resolução do classpath. Por outro lado, Rust compila a sua crate num único binário autossuficiente, seja uma biblioteca ou um executável.

\ Para ir mais longe:

  • Maven - Introdução ao Mecanismo de Dependência
  • Effective Rust - Item 25: Gerencie o seu grafo de dependência

Originalmente publicado em A Java Geek em 14 de setembro de 2025

Isenção de responsabilidade: Os artigos republicados neste site são provenientes de plataformas públicas e são fornecidos apenas para fins informativos. Eles não refletem necessariamente a opinião da MEXC. Todos os direitos permanecem com os autores originais. Se você acredita que algum conteúdo infringe direitos de terceiros, entre em contato pelo e-mail service@support.mexc.com para solicitar a remoção. A MEXC não oferece garantias quanto à precisão, integridade ou atualidade das informações e não se responsabiliza por quaisquer ações tomadas com base no conteúdo fornecido. O conteúdo não constitui aconselhamento financeiro, jurídico ou profissional, nem deve ser considerado uma recomendação ou endosso por parte da MEXC.