É incrível como o Maven, o gerenciador automático de dependências e build, lançado a dezesseis anos atrás ainda se mantém como uma das principais ferramentas do ecossistema Java, mesmo após o lançamento do Gradle, seu primo mais novo que veio com a promessa de solucionar problemas que o Maven falhou em resolver.
Mas também não é pra menos, pois o Maven é uma das ferramentas mais intuitivas e fáceis de se usar que conheço, que por utilizar o modelo ‘convenção sobre configuração’ facilita muito o trabalho do dia a dia diminuindo as escolhas que o desenvolvedor precisa fazer, proporcionando um ganho de tempo importante.
E algo que os usuários de Maven já tiveram que procurar na internet pelo menos uma vez na vida é como gerar um pacote jar com dependências, pois esse é um ponto que o Maven é falho e não oferece de forma simples.
Por isso resolvi trazer alguns métodos e exemplos de como atingir esse objetivo usando o Maven, através de plugins.
Criando um JAR
Como exemplo vamos um projeto bem simples, que somente lista os repositórios no Github de um determinado usuário.
O diferencial do projeto serão as dependências que ele utiliza (libs do Retrofit, usada para realizar requisições REST de forma fácil), que iremos colocar junto no build do Maven.
O projeto pode ser encontrado aqui, e sua execução é feita com o comando java -jar pacotejar.jar usuario-github
.
E este é o arquivo pom.xml.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tech.murilo</groupId>
<artifactId>github-user-repositories</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</manifest>
</archive>
<finalName>github-user-repositories</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
Vamos explicar alguns pontos importantes desse arquivo.
1
2
3
4
5
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Essas são as propriedades utilizadas pelo Maven no projeto, que especificam que a versão do Java utilizada é a 11 tanto a nível de compilação (source
) quanto de execução (target
), e os arquivos serão interpretados com o encoding UTF-8 (sourceEncoding
), algo que é importante se existem textos no seu código com caracteres especiais.
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
Essas são as dependências do projeto considerando também as subdependências, que se traduzem em sete arquivos jar.
- retrofit-2.7.2.jar
- okhttp-3.14.7.jar
- okio-1.17.2.jar
- converter-jackson-2.7.2.jar
- jackson-annotations-2.10.1.jar
- jackson-core-2.10.1.jar
- jackson-databind-2.10.1.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</manifest>
</archive>
<finalName>github-user-repositories</finalName>
</configuration>
</plugin>
Aqui é a configuração de build que é o que importa no assunto do post. As principais informações nessa configuração são o plugin maven-jar-plugin
, que auxilia na criação do pacote jar, as tags mainClass
que define a classe que será executada ao se executar o comando java -jar meuarquivo.jar
e a finalName
que indica o nome do arquivo que será gerado, pois caso contrário o Maven por padrão coloca o nome do artefato + versão, o que no nosso caso ficaria algo como github-user-repositories-1.0.0.jar
, interessante somente para bibliotecas e não para arquivos executáveis (runnable jar).
Ao rodar o comando mvn clean package
o maven compila e gera o pacote na pasta target
, tendo o seguinte resultado.
E agora finalmente podemos executar a aplicação com o comando java -jar target/github-user-repositories.jar murilo-ramos
.
1
2
3
4
5
6
$ java -jar target/github-user-repositories.jar murilo-ramos Exception in thread "main" java.lang.NoClassDefFoundError: retrofit2/Converter$Factory
at tech.murilo.githubuserrepositories.main.Main.main(Main.java:17) Caused by: java.lang.ClassNotFoundException: retrofit2.Converter$Factory
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 1 more
Assim podemos ver que a aplicação falhou apresentando um NoClassDefFoundError
, erro clássico indicando que alguma classe está faltando no sistema, que no nosso caso são as dependências.
JAR com dependências em diretório externo (maven-dependency-plugin)
A primeira opção que vou apresentar aqui é gerar o pacote com as dependências em um diretório (ou pasta, se preferir chamar assim) adicional, que pode ser gerado automaticamente pelo Maven.
Essa opção é bem interessante para aqueles que queiram distribuir a aplicação em partes, podendo prover somente o pacote principal e as dependências que foram alteradas, não sendo necessário prover um pacote completo toda vez que uma alteração é realizada na aplicação.
Aqui podemos usar um plugin do maven chamado maven-dependency-plugin
que em conjunto com o maven-jar-plugin
consegue gerar o pacote com as dependências.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib</classpathPrefix>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</manifest>
</archive>
<outputDirectory>${project.build.directory}</outputDirectory>
<finalName>github-user-repositories</finalName>
</configuration>
</plugin>
</plugins>
As principais informações nessa configuração são a tag de configuration/outputDirectory
no plugin maven-dependency-plugin
que direciona as dependências para uma pasta chamada lib
, e as configurações de manifest
presentes no plugin maven-jar-plugin
que dizem ao pacote que nossas dependências estarão na pasta lib
através de uma configuração de classpath
.
Agora ao rodar o comando mvn clean package
veremos nosso pacote na raiz da pasta target
e as dependências na pasta lib
.
Porém desta forma eu acredito que fica um pouco ‘bagunçado’ pois os arquivos são colocados junto aos outros arquivos produzidos pelo Maven, o que dificulta a visualização, por isso prefiro direcionar a saída para uma subpasta, algo que é super simples de se fazer bastando somente adicionar a subpasta na configuração de outputDirectory
. Vamos chamar essa subpasta de dist
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dist/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib</classpathPrefix>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</manifest>
</archive>
<outputDirectory>${project.build.directory}/dist</outputDirectory>
<finalName>github-user-repositories</finalName>
</configuration>
</plugin>
</plugins>
E finalmente conseguimos executar a aplicação com sucesso.
1
2
3
4
5
6
7
8
9
10
$ java -jar target/dist/github-user-repositories.jar murilo-ramos
Repositórios de murilo-ramos:
Nome: alura-imersao-react, Forks: 0, Estrelas: 0
Nome: alura-jquery, Forks: 0, Estrelas: 0
Nome: alura-springmvc, Forks: 0, Estrelas: 0
Nome: byte-string-format, Forks: 0, Estrelas: 0
Nome: CameraAndroid, Forks: 0, Estrelas: 0
Nome: curso-go, Forks: 0, Estrelas: 0
...
JAR com dependências em um único pacote (onejar-maven-plugin)
Se você preferir gerar um pacote contendo todas as dependências isso também é possível através do plugin onejar-maven-plugin
, que gera um pacote executável contendo o jar de nossa aplicação e as dependências em uma estrutura própria de execução.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<plugin>
<groupId>com.jolira</groupId>
<artifactId>onejar-maven-plugin</artifactId>
<version>1.4.4</version>
<executions>
<execution>
<configuration>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
<filename>github-user-repositories.jar</filename>
</configuration>
<goals>
<goal>one-jar</goal>
</goals>
</execution>
</executions>
</plugin>
No caso desse plugin a única configuração diferenciada é a definição do nome do arquivo, que nesse caso é a tag filename
devendo receber também a extensão do arquivo a ser produzido.
Executando o mvn clean package
temos o resultado.
Observe que a geração do pacote por esse plugin não remove o arquivo original github-user-repositories-1.0.0.jar
, mas não devemos nos preocupar com isso e podemos desconsiderar esse arquivo. Isso irá acontecer também nas estratégias posteriores.
1
2
3
4
5
6
7
8
9
10
11
12
$ java -jar target/github-user-repositories.jar murilo-ramos
JarClassLoader: Warning: module-info.class in lib/jackson-annotations-2.10.1.jar is hidden by lib/jackson-databind-2.10.1.jar (with different bytecode)
JarClassLoader: Warning: module-info.class in lib/jackson-core-2.10.1.jar is hidden by lib/jackson-databind-2.10.1.jar (with different bytecode)
Repositórios de murilo-ramos:
Nome: alura-imersao-react, Forks: 0, Estrelas: 0
Nome: alura-jquery, Forks: 0, Estrelas: 0
Nome: alura-springmvc, Forks: 0, Estrelas: 0
Nome: byte-string-format, Forks: 0, Estrelas: 0
Nome: CameraAndroid, Forks: 0, Estrelas: 0
Nome: curso-go, Forks: 0, Estrelas: 0
...
A maior vantagem desse plugin em relação aos outros que veremos adiante é que ele mantém de forma íntegra as dependências utilizadas e não sobrescreve nenhum arquivo no processo de construção do pacote.
Infelizmente sua desvantagem é que parece ter sido abandonado pela equipe de desenvolvimento, pois não recebe mais atualizações desde o final de 2011.
JAR com dependências em um único pacote (maven-assembly-plugin)
A segunda alternativa para gerar um único pacote é o famoso maven-assembly-plugin
.
Este plugin consegue também gerar um pacote de nossa aplicação incluindo as dependências, porém com a diferença que as dependências são descompactadas e agregadas ao pacote, fazendo com que a aplicação se torne uma coisa só.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>github-user-repositories</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifest>
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
As configurações mais importantes aqui são a tag descriptorRefs/descriptorRef
que indica qual mecanismo será utilizado para criar o pacote, onde estamos usando o padrão e pré-definido jar-with-dependencies
para gerar o pacote com as dependências, e a tag appendAssemblyId
que setamos para false
pois caso contrário o pacote gerado ficará com o nome padrão, incluindo a versão no nome do arquivo.
Executando o mvn clean package
obtemos o arquivo.
1
2
3
4
5
6
7
8
9
10
$ java -jar target/github-user-repositories.jar murilo-ramos
Repositórios de murilo-ramos:
Nome: alura-imersao-react, Forks: 0, Estrelas: 0
Nome: alura-jquery, Forks: 0, Estrelas: 0
Nome: alura-springmvc, Forks: 0, Estrelas: 0
Nome: byte-string-format, Forks: 0, Estrelas: 0
Nome: CameraAndroid, Forks: 0, Estrelas: 0
Nome: curso-go, Forks: 0, Estrelas: 0
...
Esse plugin, no entanto, é recomendável somente para projetos médios e pequenos, pois ele pode provocar conflitos no caso de classes com o mesmo nome em dependências distintas, e também ocorre sobrescrita de arquivos de mesmo nome, onde configurações podem ser perdidas se isso ocorrer dentro da aplicação e dependências (principalmente com arquivos properties).
Para resolver esse problema temos o shade, o próximo da lista.
JAR com dependências em um único pacote (maven-shade-plugin)
E para finalizar, o último método e plugin utilizado será o maven-shade-plugin
, que basicamente produz um resultado bem parecido com o plugin anterior, mas possui algumas opções e configurações a mais como é o caso da realocação da classes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>tech.murilo.githubuserrepositories.main.Main</mainClass>
</transformer>
</transformers>
<finalName>github-user-repositories</finalName>
</configuration>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
As configurações aqui não fogem muito do que já foi visto, com exceção da tag específica transformers/transformer
onde é definida a estratégia para criação do pacote. Neste caso estamos usando o ManifestResourceTransformer
que adiciona entradas no arquivo de manifesto. Outras opções podem ser vistas nesse link.
Após um mvn clean package
temos os arquivos.
1
2
3
4
5
6
7
8
9
10
$ java -jar target/github-user-repositories.jar murilo-ramos
Repositórios de murilo-ramos:
Nome: alura-imersao-react, Forks: 0, Estrelas: 0
Nome: alura-jquery, Forks: 0, Estrelas: 0
Nome: alura-springmvc, Forks: 0, Estrelas: 0
Nome: byte-string-format, Forks: 0, Estrelas: 0
Nome: CameraAndroid, Forks: 0, Estrelas: 0
Nome: curso-go, Forks: 0, Estrelas: 0
...
E é isso!
Ficou com alguma dúvida ou conhece alguma outra forma de gerar um JAR com dependências? Compartilhe comigo!
O código completo pode ser encontrado no Github.