Posts Criando um JAR com dependências usando o Maven
Post
Cancel

Criando um JAR com dependências usando o Maven

É 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.

target maven-jar-plugin

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.

target maven-dependency-plugin

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>

target maven-dependency-plugin dist

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.

target onejar-maven-plugin

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.

target maven-assembly-plugin

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.

target maven-shade-plugin

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.

Este post está licenciado sob a CC BY 4.0 pelo autor.