Mostrando postagens com marcador maven. Mostrar todas as postagens
Mostrando postagens com marcador maven. Mostrar todas as postagens

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.

quinta-feira, 10 de janeiro de 2013

Criando um sistema multi-módulos com o Maven


Nota: Uma versão atualizada e melhorada desse post está disponível no meu novo blog, com o título Criando uma aplicação multimódulo com Maven. Não deixe de conferir!!!


Imagine que você está construindo um sistema grande, composto de diversos projetos no seu IDE favorito. Cada um desses projetos tem a sua configuração Maven específica para fazer a compilação e geração do seu Jar, War, Ear ou algo similar. Apesar dessa abordagem ser satisfatória ela pode ter algumas desvantagens, tais como:

1) Cada projeto tem uma configuração totalmente separada e, caso exista algo em comum entre esses projetos, essa configuração deverá ser replicada;
2) Quando o sistema com todos os seus projetos tiverem que ser compilados, será necessário compilar cada um dos projetos considerando a dependência entre eles. E essa dependência geralmente está na cabeça de algumas pessoas que nunca estão disponíveis no momento.

Uma forma de resolver essas questões consiste em reogarnizar os projetos na estrutura multi-módulo do Maven. Nessa abordagem, cada projeto é definido como um módulo e é criado um projeto, que chamaremos de projeto agregador, utilizado agrupar todos os módulos. O projeto agregador contém um pom.xml com declarações comuns, tais como dependências e plugins, que são herdadas por cada módulo.

Para exemplificar como é essa organização multi-módulos utilizaremos um sistema fictício chamado SistemaX. Esse sistema contém dois módulos criativamente chamados de Módulo A e Módulo B. Só para complicar um pouquinho, vamos supor que o Módulo A tem uma dependência para o JUnit e o Módulo B depende do Módulo A. A estrutura de diretórios deve ficar da seguinte forma:

sistemaX/
├── moduloA
│   └── pom.xml
├── moduloB
│   └── pom.xml
└── pom.xml
O diretório sistemaX é onde está definido o projeto agregador e os diretórios dos módulos A e B são, respectivamente, moduloA e moduloB. Observe que os módulos devem ficar dentro do diretório do projeto agregador. Cada módulo continua com o seu pom.xml específico porém, agora eles devem herdar as definições feitas no pom do projeto agregador. Vamos analisar cada um dos arquivos pom.xml e explicar como são feitas as configurações.

sistemaX/pom.xml


<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>meu.projeto.multimodulo</groupId>
  <artifactId>sistemaX</artifactId>
  <packaging>pom</packaging>
  <version>1.0</version>

  <modules>
    <module>moduloA</module>
    <module>moduloB</module>
  </modules>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.8.1</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>
No pom.xml do projeto agregador são definidas as características que basicamente todo pom.xml define, tais como: groupId, artifactId, packaging e version. Um detalhe aqui é que o packaging está definido como pom. Isso indica que esse projeto não gera um artefato tipo jar ou war, e sim que ele contém definições que serão herdadas por outros projetos. A tag <modules> é usada para definir os módulos que fazem parte do projeto, nesse caso, moduloA e moduloB. Além da definição dos módulos, também são declaradas algumas dependências com a tag <dependencyManagement>. Apesar do projeto agregador em sí não precisar dessas dependências, ele pode ser usado para manter um ponto central de gerenciamento dessas definições ao invés de fazer separado por módulo.

sistemaX/moduloA/pom.xml


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

  <parent>
    <groupId>meu.projeto.multimodulo</groupId>
    <artifactId>sistemaX</artifactId>
    <version>1.0</version>
  </parent>

  <artifactId>moduloA</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
  </dependencies>
</project>


No pom.xml do Módulo A é utilizada a tag <parent> para informar que as definições feitas no pom.xml do projeto agregador são utilizadas nesse módulo. Observe que aqui não é mais necessário definir um groupId e version para o módulo pois essas informações são obtidas a partir do pom do projeto agregador. No caso desse módulo a tag <packaging> é definida como jar pois o módulo vai gerar um jar específico, o mesmo vale o módulo B, como veremos a seguir. Como esse módulo depende do JUnit, é criada uma entrada especificando essa dependência, mas repare que não é mencionada qual a versão do JUnit será usadao pois isso é obtido na definição da dependência no pom do projeto agregador.

sistemaX/moduloB/pom.xml


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

  <parent>
    <groupId>meu.projeto.multimodulo</groupId>
    <artifactId>sistemaX</artifactId>
    <version>1.0</version>
  </parent>

  <artifactId>moduloB</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>meu.projeto.multimodulo</groupId>
      <artifactId>moduloA</artifactId>
      <version>1.0</version>
    </dependency>
  </dependencies>
</project>



Note que o pom.xml do Módulo B é muito parecido com o do Módulo A, exceto pelo fato dele definir uma dependência para o Módulo A mas não para o JUnit. Nesse caso, se existir alguma classe que faça uso do JUnit no Módulo B, será gerado um erro de compilação pois a dependência não foi declarada para esse módulo, mesmo que ela tenha sido definida no pom do projeto agregador.

Pronto, feitas essas alterações basta executar o maven a partir do diretório sistemaX que ele se encarregará de compilar os projetos dependentes e gerar os respectivos artefatos que forem necessário.

Um último detalhe. Estão lembrados que o Módulo B depende do Módulo A? Assim, o Módulo A tem que ser compilado antes do Módulo B. E se no pom.xml do projeto agregador invertermos a ordem de declaração dos módulos, deixando da seguinte forma:

  <modules>
    <module>moduloB</module>
    <module>moduloA</module>
  </modules>


Bem, isso não afeta em nada! O Maven é esperto o suficiente para decifrar a dependência entre os módulos e fazer a compilação na ordem correta.

É isso ai!

Bons builds para todos.