terça-feira, 2 de abril de 2013

Gerando um único arquivo Jar com o Maven contendo todas as dependências do projeto

A construção de um projeto Java geralmente envolve o uso de diversas bibliotecas externas que podem ser desenvolvidas por terceiros ou construídas in-house especificamente para um determinado projeto. Essas bibliotecas são normalmente distribuídas como arquivos JARs e disponibilizadas nos ambiente de execução (JVM) utilizando o classpath. Apesar dessa ser uma forma padrão adotada em aplicativos construídos em Java, em certos casos temos a necessidade de gerar um único arquivo JAR que contém tanto as classes do projeto quanto as classes de suas dependências. Esse JAR em questão é conhecido como Uber JAR.

Uma abordagem direta e mais imediata que vem na cabeça para gerar esse Uber JAR é extrair os arquivos de todos JARs que o nosso projeto depende em um único diretório e depois adicioná-los novamente em um único JAR. Mas será que precisamos fazer isso na mão? Bem, se você tem o costume de usar o Maven para compilar e empacotar seus projetos, a resposta é não! O Shade é um plugin do Maven que faz esse trabalho sujo para nós.

Vamos usar um projeto simples de exemplo para demonstrar o funcionamento do Shade. O objetivo desse projeto é simplesmente gerar uma mensagem de saudação no console utilizando o Spring Framework. No final, vamos gerar um Uber JAR executável contendo as classes do projeto e as classes e arquivos de configuração do Spring Framework. Para tanto, vamos começar construindo uma classe HelloWorld, apresentada a seguir, que contém um único método responsável por gerar a saudação.

public class HelloWorld {

    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

O Spring framework será responsável por criar a instância dessa classe. Assim, vamos criar o arquivo de configuração do Spring, que basicamente define um bean chamado helloWorld do tipo da classe HelloWorld. A listagem abaixo mostra como fica o XML de configuração do contexto do Spring.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">

    <bean id="helloWorld" class="br.com.wrpinheiro.helloworldspringshade.HelloWorld" />
</beans>

Por fim, criamos a classe que será responsável por inicializar o contexto do Spring, obter a instância do bean do contexto e chamar o método que gera a saudação. A listagem abaixo apresenta tal classe.

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("helloWorldCtx.xml");
        HelloWorld h = ctx.getBean("helloWorld", HelloWorld.class);
        String message = h.sayHello("Shade Plugin");

        System.out.println(message);
    }
}

Agora que temos todos os artefatos necessários para o nosso projeto, vamos construir o pom.xml do Maven para o nosso projeto. O conteúdo desse arquivo aparece na seguinte listagem.

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>br.com.wrpinheiro</groupId>
  <artifactId>helloworld-spring-shade</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>helloworld-spring-shade</name>
  <url>http://maven.apache.org</url>

  <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>3.2.0.RELEASE</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.handlers</resource>
                </transformer>
                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.schemas</resource>
                </transformer>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>br.com.wrpinheiro.helloworldspringshade.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Esse arquivo deixa explicito na seção <dependencies> que o projeto depende do Spring Context. Na seção <build> é informado que o plugin Shade deve ser usado na fase de empacotamento (package) do projeto, ou seja, durante a geração do JAR do projeto deverá ser executado o Shade para copiar para o JAR gerado as classes que o projeto depende. Note que o Uber JAR gerado conterá todas as classes de todas as dependências resolvidas transitivamente, ou seja, as classes do Spring Context, as dependências do Spring Context, as dependências das dependências, e assim por diante.

Note ainda que nesse pom usamos os Resources Transformers do Shade na seção <configuration>. Os Resource Transformers são usado para agregar diversos recursos em um único arquivo. Por exemplo, no caso do Spring Framework, cada JAR contém um conjundo de arquivos XSD utilizados para validar os XMLs e dentro do META-INF de cada JAR existem dois arquivos chamados spring.schemas e spring.handlers que auxiliam na tarefa de "descobrir" onde estão os arquivos XSD. Como estamos juntando vários arquivos JARs do Spring e em cada um desses arquivos existe o spring.schemas e o spring.handlers, será necessário unir o conteúdo desses arquivos em um único arquivo que ficará no META-INF do Uber Jar. Essa junção dos arquivos pode ser feita pelo Shade, utilizando as seguintes definições:

    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
      <resource>META-INF/spring.handlers</resource>
    </transformer>
    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
      <resource>META-INF/spring.schemas</resource>
    </transformer>

Além disso, como o JAR gerado será um executável vamos incluir também no MANIFEST-MF qual é a classe executável do projeto, incluindo as seguintes linhas no pom.xml:

    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
      <mainClass>meu.projeto.helloworld.Main</mainClass>
    </transformer>

Nosso projeto está pronto para ser compilado e testado, conforme mostrado a seguir:

$ mvn package

[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building helloworld-spring-shade 1.0
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ helloworld-spring-shade ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ helloworld-spring-shade ---
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 2 source files to /opt/workspaces-eclipse/helloworld-spring-shade/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ helloworld-spring-shade ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /opt/workspaces-eclipse/helloworld-spring-shade/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ helloworld-spring-shade ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ helloworld-spring-shade ---
[INFO] No tests to run.
[INFO] Surefire report directory: /opt/workspaces-eclipse/helloworld-spring-shade/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
There are no tests to run.

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ helloworld-spring-shade ---
[INFO] Building jar: /opt/workspaces-eclipse/helloworld-spring-shade/target/helloworld-spring-shade-1.0.jar
[INFO] 
[INFO] --- maven-shade-plugin:2.0:shade (default) @ helloworld-spring-shade ---
[INFO] Including org.springframework:spring-context:jar:3.2.0.RELEASE in the shaded jar.
[INFO] Including org.springframework:spring-core:jar:3.2.0.RELEASE in the shaded jar.
[INFO] Including commons-logging:commons-logging:jar:1.1.1 in the shaded jar.
[INFO] Including org.springframework:spring-aop:jar:3.2.0.RELEASE in the shaded jar.
[INFO] Including aopalliance:aopalliance:jar:1.0 in the shaded jar.
[INFO] Including org.springframework:spring-expression:jar:3.2.0.RELEASE in the shaded jar.
[INFO] Including org.springframework:spring-beans:jar:3.2.0.RELEASE in the shaded jar.
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing /opt/workspaces-eclipse/helloworld-spring-shade/target/helloworld-spring-shade-1.0.jar with /opt/workspaces-eclipse/helloworld-spring-shade/target/helloworld-spring-shade-1.0-shaded.jar
[INFO] Dependency-reduced POM written at: /opt/workspaces-eclipse/helloworld-spring-shade/dependency-reduced-pom.xml
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.326s
[INFO] Finished at: Tue Apr 02 11:35:45 BRT 2013
[INFO] Final Memory: 9M/239M

$ cd target

$ java -jar helloworld-spring-shade-1.0.jar
02/04/2013 11:36:26 org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@296672d6: startup date [Tue Apr 02 11:36:26 BRT 2013]; root of context hierarchy
02/04/2013 11:36:26 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [helloWorldCtx.xml]
02/04/2013 11:36:26 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@70453807: defining beans [helloWorld]; root of factory hierarchy
Hello, Shade Plugin!

Observe que no diretório target do projeto existem dois arquivos JAR: (i) helloworld-spring-shade-1.0.jar, que é o Uber JAR e (ii) original-helloworld-spring-shade-1.0.jar, que é o arquivo JAR original sem as dependências.

O código fonte do projeto de demonstração está disponível no Github, nesse link.

Por enquanto é isso aí galera! Bons builds.

Abraços,
Wellington.

4 comentários:

  1. Wellington, você sabe qual a diferença entre usar o maven-shade-plugin e o maven-assembly-plugin, com a configuração jar-with-dependencies.

    Você sabe se daria para configurar o maven-shade-plugin para EVITAR que somente uma dependência seja incluída no Uber-jar?

    Valeu pelo tutorial.

    []s
    mario

    ResponderExcluir
    Respostas
    1. Desculpe,

      mas eu havia passado os olhos por cima no site do shade e não vi que lá tinha a resposta para minha pergunta. Tem uma configuração chamada DontIncludeResourceTransformer...

      Excluir
    2. Sou eu novamente.

      Wellington, obrigadão por esse post. Pois como eu usava o maven-assembly-plugin há muito tempo eu nem ao menos pensei em procurar uma solução para meu problema no site da apache.

      Usei a solução apresentada na documentação do shade, http://maven.apache.org/plugins/maven-shade-plugin/examples/includes-excludes.html

      Valeu :)
      mario

      Excluir
    3. Fala Mario, tudo bem contigo? Já é PhD?

      Sobre a sua pergunta, você consegue gerar um Uber Jar tanto com o Shade quanto com o Assembly porém, o Shade tem algumas configurações que o torna mais flexível, como por exemplo os Resource Transformers. Com eles você consegue adicionar conteúdo aos arquivos XML durante a geração do Jar, fazer o merge do conteúdo do META-INF dos JARs usados para gerar o META-INF final no Uber Jar, e algumas outras coisinhas.

      Nesse link você encontra mais detalhes sobre os Resource Transformers:
      http://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html

      No exemplo desse post eu precisava pegar os arquivos do Spring spring.handlers e spring.schema que estavam no META-INF de dois JARs e copiar para o Uber Jar gerado. Com o Shade isso é simples de fazer mas com o Assembly nem tanto (se é que tem como fazer :-).

      Isso ae, espero ter ajudado!

      []'s,
      Wellington.

      Excluir