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

segunda-feira, 15 de dezembro de 2014

How to Assemble Multi-Module Project with Maven

Assembling multi-module projects with Maven can be tricky. Based on different forum posts and on books, I finally could set my project to assemble for distribution.

I created prototypes to validate my ideas, using Maven 3.2.3. For learning purposes, my prototype multi-module project is like this:

Application project's structure.
  • application: it contains the parent pom file with a few and important configurations.
  •  app-distribution: created only to assemble the whole project for distribution; it has no code and no resource, only pom file configuration, and an assembly descriptor file; this approach is mention as a 'low-tech' best practice.
  • app-gui: the graphical user interface; this is the main application actually; it has resources, some included in the jar file and some to be distributed outside the jar file.
  • app-model: imaginary application model with some resources to de distributed outside the jar file.
  • app-utils: imaginary utility classes.
Besides the app-distribution, the other modules have dependencies among them and on third-party libraries, like this:

Application project's dependencies.

Before assembling the project, let's take care of the resources that must go outside of the jar file; otherwise, they will be zipped inside the jar file since they are placed in the default \src\main\resources folder. The piece of code below belongs to the pom file of the app-gui and app-model modules to exclude files with 'ini' extension:

    <build>
        <resources>
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
                <excludes>
                    <exclude>**/*.ini</exclude>
                </excludes>               
                <filtering>false</filtering>               
            </resource>            
        </resources>
    </build>

To assemble the whole project, modules with resources that go outside of the jar file must be bundled first, that is, the app-gui and app-model modules. Those modules are bundled only with their external resources; their main jar file and libs are left out at this stage.

To make that work, an assembly descriptor file must be created, and the maven-assembly-plugin must be configured to do so for each module. For this project, that is not a big deal since we have only two modules to set up, but for large projects that might be cumbersome.

So, let's think of reuse!

Firstly, even before compiling the application projects, a new project come into play, created completely independent of the application project mentioned previously; the bundle project has only an XML file which is the assembly descriptor file. It is placed in the /src/main/resources/assemblies folder (that's important), and it is named 'bundle-descriptor.xml':

    <assembly>
        <id>bundle</id>
        <formats>
            <format>zip</format>
        </formats>
        <includeBaseDirectory>false</includeBaseDirectory>
        <fileSets>
            <!-- Add external resources -->
            <fileSet>
                <directory>src/main/resources/properties</directory>
                <outputDirectory>properties</outputDirectory>
                <useDefaultExcludes>true</useDefaultExcludes>
            </fileSet>
            <fileSet>
                <directory>src/main/resources/xsd</directory>
                <outputDirectory>xsd</outputDirectory>
                <useDefaultExcludes>true</useDefaultExcludes>
            </fileSet>
        </fileSets>
    </assembly>

Execute the following command to package and install the JAR file in maven repository before working on the application project:

mvn package install

The bundle-descriptor.xml file only handles external resources; it zips them in a file to be deployed to the maven repository. The module's jar file and libs are left out.

To make use of the bundled project, the maven-assembly -plugin has to be configured the same way for both app-model and app-gui modules. Instead, only one configuration can be placed in the parent pom, within <build> and <pluginanagement> tags, to be reused later by those modules:

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <version>2.5.2</version>
                    <dependencies>
                        <dependency>
                            <groupId>blog.dacanal.assembly</groupId>
                            <artifactId>bundle</artifactId>
                            <version>1.0.0</version>
                        </dependency>
                    </dependencies>
                    <executions>
                        <execution>
                            <id>assemble</id>
                            <phase>package</phase>
                            <goals>
                                <goal>single</goal>
                            </goals>
                            <configuration>
                                <descriptorRefs>
                                    <descriptorRef>bundle-descriptor</descriptorRef>
                                </descriptorRefs>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

The bundle project is set as a dependency of the maven-assembly-plugin, and the assembly descriptor file name is set within the <descriptorRef> tags. That way, the bundle project and the configuration of the maven-assembly-plugin can be reused by those modules that make reference to the plugin in their pom file, simply like this:

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Now it's time to work on the app-distribution module. The whole application is distributed with the main jar file in the root directory, the other dependent modules and third party libs in the \lib directory, and the external resources in their own directories.

Some dependencies have to be set in the pom file of the app-distribution module. The app-gui is the main module; it becomes a dependency here, and it has the complete dependency tree of the application. The external resources of the app-gui and app-model modules are recovered by setting them as dependencies with <type> tag as 'zip' and <classifier> tag as 'bundle' (that's the id of the bundle-descriptor XML file).

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>gui</artifactId>
            <version>${project.version}</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>gui</artifactId>
            <version>${project.version}</version>
            <type>zip</type>
            <classifier>bundle</classifier>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>model</artifactId>
            <version>${project.version}</version>
            <type>zip</type>
            <classifier>bundle</classifier>
        </dependency>
    </dependencies>

The app-distribution module has its own assembly descriptor XML file with a configuration to mount our application for distribution. The descriptor file handles all dependencies (including the dependency tree) of the app-distribution module, unpacking them, or placing them on specific directories. The code of the assembly descriptor file is presented below; I added some comments to it, explaining a bit of each set of <dependencySet> tag:

    <assembly>
        <id>distribution</id>
        <formats>
            <format>zip</format>
        </formats>
        <baseDirectory>${appName}</baseDirectory>
        <dependencySets>
            <!-- Unpack all dependencies of ZIP type and BUNDLE classifier,  -->
            <!-- and unpack them in the root folder.                         -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>false</useTransitiveDependencies>
                <unpack>true</unpack>
                <includes>
                    <include>*:zip:bundle</include>
                </includes>
            </dependencySet>
            <!-- Get the main module and place it in the root dir. -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>false</useTransitiveDependencies>
                <unpack>false</unpack>
                <includes>
                    <include>*:*:jar</include>
                </includes>
            </dependencySet>      
            <!-- Get all dependency tree libs and place them in \lib folder, -->
            <!-- except those of ZIP type and BUNDLE classifier, and the     -->
            <!-- the main module. -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>true</useTransitiveDependencies>            
                <unpack>false</unpack>
                <outputDirectory>lib</outputDirectory>
                <excludes>
                    <exclude>*:zip:bundle</exclude>
                    <exclude>${project.groupId}:gui:*</exclude>
                </excludes>
            </dependencySet>
        </dependencySets>    
    </assembly>

The configuration of the maven-assembly-plugin cannot be reused here. Actually, it has to be avoided because it is unnecessary to get the app-distribution module assembled every time the whole project is compiled and packaged (the application is not intended to be distributed on a daily basis). Then, a new configuration is created in the pom file of the app-distribution module, as described below (with comments):

    <build>
        <plugins>           
            <!-- Plugin required to assemble the project for distribution  -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.2</version>
                <!-- Not attached to any execution phase; it can only be invoked with 'mvn assembly::assembly' command. -->
                <configuration>
                    <descriptors>
                        <descriptor>src/main/assemblies/assembly-descriptor.xml</descriptor>
                    </descriptors>
                    <finalName>${appName}-${project.version}</finalName>                 
                </configuration>
                <!-- No need to run every time this module is compiled and packaged. -->
                <!-- Overwrite the parent pom configuration.                         -->
                <executions>
                    <execution>
                        <id>assemble</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <skipAssembly>true</skipAssembly>
                        </configuration>
                    </execution>
                </executions>              
            </plugin>
        </plugins>
    </build>

To get the application assembled, the user has to type the maven command in the app-distribution directory:

mvn assembly:assembly

I've been using this approach to assemble and distribute applications at work. I hope I have made myself clear and hope this post can help others to tailor their own solution using Maven.

The code I developed to validate this approach is available for download on Google Drive.

Steps To Make It Work


1) For those who are new using maven: before compiling the project modules, the parent pom file must be deployed to the local repository; run this command line from the parent directory (\application):

mvn install:install-file -DgroupId=blog.dacanal -DartifactId=application -Dversion=1.0-SNAPSHOT -Dpackaging=pom -Dfile=pom.xml

2) Package and install the JAR file of the assembly project in maven repository (\assembly):

mvn package install

3) Now, the same for the application project (\application):

mvn package install

4) Enter the app-distribution directory (\application\app-distribution) and assembly the project:

mvn assembly:assembly

The app-project will be assembled in \application\app-distribution\target\application-1.0-SNAPSHOT-distribution folder.

sexta-feira, 21 de fevereiro de 2014

Projeto EAR com DLLs

Pelos últimos artigos deste blog, percebe-se que trabalho com C++ e Java e não dispenso a utilização do Maven nos meus projetos. O nar-maven-plugin permite que eu desenvolva alguma coisa em C++ usando o Maven e, o melhor, ele permite Java e C++ no mesmo projeto, mantendo casadas as interfaces JNI.

Recentemente tive que utilizar algumas bibliotecas Java e C++ num projeto web com Enterprise Java Beans, empacotado como EAR (Enterprise Archive). O Maven tem um plugin específico para empacotar este tipo de projeto, chamado de maven-ear-plugin, mas com limitações com relação aos tipos de artefatos aceitos para empacotamento e as DLLs não são aceitas neste caso.

O Problema

Para entender melhor, a figura abaixo exemplifica, de forma simplificada, o problema no meu projeto. O módulo NAR produz um artefato de projeto do tipo NAR, pelo nar-maven-plugin. Este artefato de projeto contém um arquivo JAR e uma DLL compactados. Este módulo é um projeto com interface JNI entre o Java e o C++.  A dependência entre o módulo EJB e NAR é resolvida facilmente pelo próprio nar-maven-plugin. O problema surge no módulo EAR, quando o pluging do Maven, que faz o empacotamento, não consegue tratar artefatos do tipo NAR ou mesmo do tipo DLL.


Mesmo que o plugin conseguisse empacotar o artefato NAR no módulo EAR, o problema continuaria porque os arquivos JAR e DLL contidos no artefato NAR precisariam ser extraídos.

A Solução 

A solução para este problema consiste em i) modificar a dependência entre os módulos EJB e NAR, ii) empacotar a DLL no arquivo EJB, iii) incorporar os byte codes (arquivos class) do arquivo JAR (do módulo NAR) no arquivo EJB e iv) desempacotar a DLL em tempo de execução.

Passo 1

A dependência do projeto NAR pelo projeto EJB precisa ser modificada para que o projeto EAR a ignore. Em outras palavras, ao empacotar o projeto EAR, o Maven, quando desenrola a cadeia de dependências, precisa ignorar a dependência do projeto EJB pelo projeto NAR.

Para tanto, no projeto EJB (arquivo pom.xml) é preciso definir a tag OPTIONAL como TRUE para descrição de dependência do projeto NAR., conforme mostrado abaixo:

<dependency>
    <groupid>blog.dacanal</groupid>
    <artifactid>jni</artifactid>
    <version>1.0-SNAPSHOT</version>
    <type>nar</type>
    <optional>true</optional>
</dependency>

O mesmo seria necessário caso a dependência fosse um módulo do tipo DLL.

Passo 2

Ao gerar o módulo EJB, a DLL precisa ser empacotada no arquivo EJB como um recurso. O maven-ant-plugin pode ser utilizado para copiar, no instante correto, a DLL para a pasta onde os byte codes foram gerados. A configuração do plugin é apresentada a seguir:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.7</version>
    <executions>
        <execution>
            <id>copy-resources</id>
            <phase>process-resources</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <target>
                    <copy file="${basedir}\target\nar\jni-1.0-SNAPSHOT-x86-Windows-gpp-jni\lib\x86-Windows-gpp\jni\jni-1.0-SNAPSHOT.dll" todir="${basedir}\target\classes\lib" />
                </target>                                        
            </configuration>            
        </execution>
    </executions>
</plugin>

Para que o maven-ant-plugin faça seu trabalho corretamente, antes o nar-maven-plugin precisa ser configurado no projeto EJB para que ele descompacte a DLL, do arquivo NAR, no instante adequado. Lembre-se de que o artefato NAR está no repositório Maven (local ou remoto).

<plugin>
    <groupId>com.github.maven-nar</groupId>
    <artifactId>nar-maven-plugin</artifactId>    
    <version>3.0.1-SNAPSHOT</version>  
    <extensions>true</extensions>  
    <executions>
        <execution>
            <id>unpack-nar</id>
            <goals>
                <goal>nar-unpack</goal>
            </goals>
            <phase>process-sources</phase>
            <configuration>                                                                                                                     
            </configuration> 
        </execution>
    </executions>                                          
</plugin>

Passo 3

Falta ainda incorporar os byte codes do projeto NAR no projeto EJB. Agora, o maven-dependecy-plugin faz este trabalho, conforme configurado abaixo:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>2.8</version>
    <executions>
        <execution>
            <id>unpack-jar</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>blog.dacanal</groupId>
                        <artifactId>jni</artifactId>
                        <version>1.0-SNAPSHOT</version>
                        <type>nar</type>
                        <overWrite>true</overWrite>
                        <outputDirectory>${project.build.directory}\classes</outputDirectory>
                        <includes>**/*.class</includes>
                        <excludes>**/*test.class</excludes>
                    </artifactItem>
                </artifactItems>                           
            </configuration>
        </execution>
    </executions>
</plugin> 

As configurações Maven apresentadas nos passos 2 e 3, todas realizadas no módulo EJB, são importantes mas o trabalho ainda não terminou.

Passo 3

A DLL, agora encapsulada no módulo EJB, precisa ser desencapsulada no instante em que for utilizada. Esta operação é realizada por código Java, em tempo de execução. No caso exemplificado, o código deve ser incorporado ao módulo EJB. Veja abaixo a codificação:

public static void extractNativeLibraries() throws IOException {
    // Read the jar       
    CodeSource src = DataLoaderProxy.class.getProtectionDomain().getCodeSource();
    if (src != null && src.getLocation().getFile().endsWith(".jar")) {
        URL jar = src.getLocation();
        String filePath = jar.getPath();
        String outputPath = "c:\\temp"; // not a good idea!
        JarFile jarFile = new JarFile(new File(filePath));
        unpackDLLs(jarFile, outputPath);
    } 
}

private static void unpackDLLs(JarFile jarFile, String destDir) throws FileNotFoundException, IOException {
    for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) {
        JarEntry entry = entries.nextElement();
        if (entry.getName().endsWith(".dll")) {
            int i = entry.getName().lastIndexOf('/');
            File outputFile = new File(destDir + "\\" + entry.getName().substring(i > 0 ? i + 1 : 0));
            outputFile.getParentFile().mkdirs();
            try (FileOutputStream out = new FileOutputStream(outputFile); InputStream in = jarFile.getInputStream(entry);) {
                byte[] buffer = new byte[8 * 1024];
                int s;
                while ((s = in.read(buffer)) > 0) {
                    out.write(buffer, 0, s);
                }
                out.flush();
            }
        }
    }
}

O método extractNativeLibraries precisa ser executado antes do carregamento da DLL (é lógico). O importante aqui é o caminho onde a DLL será descompactada. Caso o caminho não esteja entre os caminhos de busca do sistema operacional (variável de ambiente PATH do Windows, por exemplo), é necessário modificar a variável java.library.path da JVM. Veja o artigo java.lang.UnsatisfiedLinkError para a solução deste problema.

Referências:

quinta-feira, 12 de dezembro de 2013

JNI: Java e C++ Num Único Projeto Maven

Desenvolver projetos que empreguem a tecnologia JNI necessariamente não significa ter um projeto para Java e um outro para C++ (ou C) separados. Com Maven e o plugin nar-maven-plugin é possível ter os dois projetos como se fossem um só. O plugin permite a compilação do código Java, a geração dos cabeçalhos JNI em C++/C, a compilação do código C++/C e a execução dos testes unitários, tudo isso com pouca configuração.

Além de uma configuração fácil, as vantagens práticas de se usar o nar-maven-plugin para projetos com JNI são:
  1. Os cabeçalhos JNI em C++/C são gerados automaticamente;
  2. Os códigos C++ e Java são compilados juntos;
  3. Os artefatos gerados, sendo o JAR e DLL, sempre estarão casados com o mesmo número de versão;
  4. O nome da DLL (que inclui o número da versão conforme o padrão Maven) é automaticamente ajustado no código Java para seu carregamento na JVM;
  5. Projetos que dependem da biblioteca Java com JNI fazem apenas uma referência de dependência que inclui tanto o JAR quanto a DLL;
Cada item descrito acima merece uma descrição mais elaborada que será apresentada no restante do artigo. Outra grande vantagem, que não será explorada neste artigo, é possibilidade de gerar diferentes versões de DLL para cada tipo de plataforma de sistema operacional.

Além das vantagens de se usar o nar-maven-plugin, ainda existem outras vantagens de se usar o Maven também para o projeto C++/C.

O detalhamento da utilização da tecnologia JNI também não é abordado neste artigo, além no necessário para se entender o projeto.

Objetivos do Projeto JNI

O projeto apresentado aqui é para fins didáticos com o objetivo de testar o nar-maven-plugin, compilando código Java e C++ num único projeto, mais a execução de um teste unitário JUnit.

A parte Java do projeto constitui apenas de uma classe com a definição de um método nativo para chamada do código em C++. Assim a parte C++ consiste na implementação da função JNI cujo cabeçalho é gerado pelo plugin. Um teste unitário é criado para validar a codificação.

Tanto o código C+++ quanto o código Java  não serão detalhado neste artigo, mas estão disponíveis para download para melhor entendimento do projeto.

Preparação do Ambiente

O projeto foi compilado e testado na plataforma Windows 7 64 bits utilizando os seguintes sistemas:
Para o Maven, Mingw e Java, seus diretórios de instalação (com \BIN) devem ser incluídos manualmente no PATH do Windows.

O nar-maven-plugin e o JUnit serão baixados automaticamente pelo Maven.

Embora a plataforma de desenvolvimento seja 64 bits, o MingW compila código apenas para plataforma 32 bits. Por este motivo, é importante que o JDK também seja 32 bits para que seja possível o carregamento da DLL pela JVM.

Download

O código do projeto está disponível para download pelo Google Drive. Embora seja simples, ele serve de modelo inicial para projetos reais.


O código também serve para aqueles mais versados nas tecnologias envolvidas aqui, para que possam aprender mais rapidamente sem ter que ler todo o artigo, apenas analisando os códigos.

Configuração do Projeto JNI

O arquivo pom.xml contem todas as configurações do projeto. O arquivo completo não é apresentado aqui, mas as partes mais relevantes são apresentadas e comentadas, ou seja, as configurações mínimas exigidas pelo Maven estão suprimidas. Obtenha o arquivo pom.xml completo fazendo o download do projeto

 Packaging

A primeira coisa que se deve fazer num projeto que utiliza o nar-maven-plugin é alterar a configuração (obrigatória) de packaging do  Maven. Num projeto java, esta configuração é definida como JAR, mas com o nar-maven-plugin ela tem que ser definida como NAR, conforme abaixo:

<!-- Importante que JAR seja substituído por NAR -->
<packaging>nar</packaging>

Dependências

Apenas a dependência do JUnit para a execução do teste unitário deve ser descrita no arquivo de configuação pom.xml. A configuração de dependência é padrão do  Maven; veja que o escopo da dependência é apenas para teste:

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Plugin

A configuração do nar-maven-plugin segue abaixo:

<build>
    <plugins>            
        <plugin>
            <groupId>com.github.maven-nar</groupId>
            <artifactId>nar-maven-plugin</artifactId>    
            <version>3.0.0</version>  
            <extensions>true</extensions>
            <configuration>                                                     
                <cpp>                                                        
                    <name>g++</name> 
                </cpp>
                <linker>
                    <name>g++</name>
                    <options> 
                        <option>-Wl,--kill-at</option>               
                    </options>
                </linker>
                <libraries>
                    <library>
                        <type>jni</type>
                        <narSystemPackage>com.prototype.jni.project</narSystemPackage>
                    </library>                        
                </libraries>                     
                <javah>                 
                    <jniDirectory>${basedir}\src\main\c++</jniDirectory>
                    <classDirectory>${project.build.outputDirectory}\com\prototype\jni\project</classDirectory>                       
                </javah> 
            </configuration>             
        </plugin>
    </plugins>
</build>

A declaração do plugin é bastante trival e segue os convenções do Maven. A partir da tag <configuration> estão as configurações particulares do plugin. Estas configurações, suas particularidades e funcionamento do plugin são descritos abaixo

O nar-maven-plugin tem compiladores e linkers definidos como padrão (default) de acordo com o sistema operacional. No caso do Windows, o padrão é o Microsoft Visual C++. Se este fosse o caso do projeto apresentado, não seria necessário definir o nome do compilador C++ e do linker (que seria mvsc). Portanto, como o Mingw é o compilador e o linker utilizados no projeto, o plugin precisa saber disso, informando o nome como g++ para o compilador (tag <cpp>) e para o linker (tag <linker>).

Ainda sobre o linker, no caso especificamente do g++ (ou gcc), é importante passar a opção -Wl,--kill-at para que a DLL seja gerada corretamente. Sem esta opção, a máquina virtual Java, ao carregar a DLL, não encontra as funções que espera encontrar, pois o linker adicionar um sufixo @número ao nome de cada função. Na opção descrita, note que após a vírgula não existe espaço em branco, portanto estamos falando de uma única opção e não de duas opções aqui!

A tag <libraries> contém a tag <library> com o tipo de binário gerado. No caso deste projeto, o tipo válido corresponde a JNI (em minúsculo) o que significa que o código Java deve ser compilado e gerado um arquivo do tipo JAR e o código C++ (ou C) deve ser compilado e gerado um arquivo do tipo DLL. Ainda, aqui também se define o package para a classe java NarSystem cujo código é gerado automaticamente pelo plugin para carregamento da DLL JNI. Informações adicionais sobre esta classe serão apresentadas mais adiante, quando a execução do plugin é abordada.

A geração automática do cabeçalho JNI a partir de classes Java com métodos nativos é feita com a configuração da tag <javah>. Aqui, é importante configurar duas coisas: o caminho onde os cabeçalhos C++/C serão gerados (tag <jniDirectory>) e o caminho onde os byte codes das classes Java (com métodos nativos) estão localizados (tag <classDirectory>).

Propriedades

Uma propriedade em especial precisa ser declarada no arquivo pom.xml (completamente fora da declaração e configuração do plugin). A propriedade <skipTests> deve ser definida como false para que o plugin surefire do Maven não execute os testes unitários. Este plugin não precisa ser declarado no arquivo pom.xml pois é padrão do Maven.

<properties>
    <skipTests>true</skipTests>
</properties>

Com esta propriedade definida como falsa, a execução dos testes unitários fica por conta do nar-maven-plugin. A razão disto é que ele inclui no classpath o caminho da DLL JNI antes do início da execução do teste.

Construção do Projeto e Funcionamento do Plugin

Uma vez o arquivo pom.xml configurado, a classe Java, com pelo menos um método nativo, já pode ser criada num determinado pacote (package) java. Uma vez compilado o projeto, o nar-maven-plugin invoca o javah do JDK para criar automaticamente os cabeçalhos JNI (arquivos com a extensão H) no diretório \src\main\c++, conforme fora configurado pela tag <jniDirectory>.

Para cada classe com métodos nativos, tem-se um arquivo gerado com a assinatura das funções que devem ser criadas em C++ ou C. Um arquivo adicional com cabeçalho é criado pelo plugin para uma classe 'mágica' chamada de NarSystem. Este cabeçalho contém a assinatura de uma função que corresponde ao método runUnitTestsNative da classe NarSystem. Esta função pode ser implementada para que testes unitários em C++ ou em C possam se executados, porém isso não é obrigatório e não será discutido neste artigo.

A classe NarSystem é gerada no diretório \target\nar\nar-generated, mais os diretórios que compõem o pacote (package) Java onde está a classe, conforme configurado no arquivo pom.xml pela a tag <narSystemPackage>. Analisando esta classe, que é bem simples e intuitiva, ela é composta por dois métodos estáticos e um método nativo e por isso o cabeçalho JNI adicional é gerado automaticamente. Um método desta classe destina-se ao carregamento da DLL e o outro a execução dos testes unitários em C++ ou C. A vantagem na utilização desta classe para carregamento da DLL é que o número da versão do projeto que compõe o nome da DLL é corrigido automaticamente pelo pelo nar-maven-plugin, para seu carregamento adequado.

Embora a compilação tenha ocorra com sucesso até este ponto, o projeto ainda não está completo. Faltam as implementações das funções que são chamadas pelo Java. O código da implementação deve estar na pasta \src\main\c++ (ou pelo menos a partir dela) pois este é o diretório padrão que o plugin espera encontrar código C++. Este diretório, além de ser o padrão, foi exatamente o configurado pela tag <jniDirectory> no arquivo pom.xml.

Para finalizar, o teste unitário precisa ser criado para testar o código Java que invoca o código C++. O importante aqui está no carregamento da DLL JNI pelo Java. A classe NarSystem com o método estático loadLibrary faz este trabalho, mantendo o nome da DLL sincronizado com o nome do JAR e o número da versão do projeto. Este método deve ser chamado assim que o programa Java for carregado.

Uma vez tudo compilado, o nar-maven-plugin adiciona ao classpath (Java) o diretório da DLL para rodar o teste unitário.

Caso o teste seja executado com sucesso, o plugin compacta o binário JAR e DLL num único arquivo com extensão NAR e o instala no repositório local Maven. Um outro projeto que tenha dependência deste projeto precisa também utilizar o nar-maven-plugin, que se encarrega de descompactar o arquivo NAR e disponibilizar tanto o arquivo JAR quanto o arquivo DLL para uso.

Conclusão

O nar-maven-plugin é ideal para a criação de código Java e C++ que emprega a tecnologia JNI. Ele mantem um único projeto para códigos de ambos compiladores dentro dos padrões Maven e sua configuração é de baixa complexidade.

A automatização da geração de cabeçalhos com assinaturas de funções que fazem interface com Java e C++ garante que elas sempre estarão casadas. Todavia, isso não suficiente para evitar erros no projeto: mesmo que os cabeçalhos gerados com as assinaturas das funções C++ e as respectivas implementações das funções C++ estejam descasados, o código C++ compila sem erros.

O plugin ainda permite convenientemente que o nome da DLL gerado siga o padrão Maven e esteja casado com o nome do binário Java (JAR). Esta padronização de nomes tem consequência no carregamento da DLL pelo programa Java, pois caso ocorra alteração do número da versão do projeto, o nome da DLL é também alterado (e seu nome no programa Java também deveria ser alterado). O nar-maven-plugin mantem isso funcional criando automaticamente no projeto a classe NarSystem para carregar a DLL no programa Java, ajustando adequadamente o nome da DLL.

segunda-feira, 9 de dezembro de 2013

C++ e Maven: nar-maven-plugin

No meu trabalho, desenvolvo códigos em C, C++ e Java principalmente, além de outras linguagens. Para Java, já não consigo mais dispensar a utilização do Maven. Pesquisei sobre como usar Maven também para compilar códigos em C e C++. Foi então que encontrei o nar-maven-plugin.

Este plugin para Maven está atualmente ativo graças a uma grupo de desenvolvedores, mas nem sempre foi assim: ele esteve meio parado por um tempo e seu desenvolvedor original doou o código para um outro grupo de desenvolvedores continuar o trabalho. Tutoriais sobre o plugin ainda são escassos e muitos fazem referência a uma versão mais antiga e algumas configurações não se aplicam mais para a versão atual. O mailing list no momento tem poucas postagens, mas quando o assunto é relevante, os desenvolvedores estão sempre atentos e respondem com rapidez.

Uma outra opção é usar o native-maven-plugin. Este tem a sua última versão, a 1.0-alpha-7, liberada em março de 2011. Os tutoriais sobre este plugin são poucos e achei sua configuração pouco intuitiva. Por todos estes motivos desfavoráveis, optei por usar o nar-maven-plugin, principalmente por estar mais ativo no momento e ser mais um pouco mais fácil configurá-lo.

Objetivos dos Testes

A intenção aqui é criar dois projetos em C++, sendo um para gerar uma biblioteca estática e outro com uma biblioteca dinâmica que utiliza a biblioteca estática.

Os projetos são bem simples e estão estruturados de acordo com Maven 3. O foco está na utilização do nar-maven-plugin e não exatamente na utilização do Maven. Portanto, é esperado que se conheça o básico sobre Maven e sua utilização.

Preparação para os Testes

Os testes foram realizados no Windows 7 64 bits utilizando os seguintes sistemas:
  • Maven 3
  • Mingw 4.8.1
  • Nar-maven-plugin-3.0.0
    Tanto para o Maven quanto para o Mingw, os diretórios de instalação (com \BIN) devem ser incluídos manualmente no PATH do Windows.

    O nar-maven-plugin será baixado automaticamente pelo Maven de acordo com a configuração apresentada abaixo.

    Download

    O código dos projetos usados para testes estão disponíveis para download. São simples mas servem de modelo para evoluir para projetos maiores.


    Projeto Biblioteca Estática

    Este projeto serve de base para os demais. Abaixo, o arquivo pom.xml do projeto da biblioteca estática é apresentado com a configuração mínima para a utilização do nar-maven-plugin.

    <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>prototype</groupId>
        <artifactId>lib-project</artifactId>
        <version>1.0-SNAPSHOT</version>
      
        <!-- Importante que a opção JAR seja substituída por NAR -->
        <packaging>nar</packaging>
    
        <name>lib-project</name>
    
        <!-- Repositório onde está armazenado o plugin -->
        <pluginRepositories>
            <pluginRepository>
                <id>Sonatype OSS</id>
                <url>http://oss.sonatype.org/content/groups/public</url>
            </pluginRepository>
        </pluginRepositories>
    
        <build>
            <plugins>            
                <plugin>
                    <groupId>com.github.maven-nar</groupId>
                    <artifactId>nar-maven-plugin</artifactId>
                    <version>3.0.0</version>
                    <extensions>true</extensions>
                    <configuration>
                        <!-- Importante definir o nome do compilador C++ caso não seja 
                             o default definido pelo NAR plugin de acordo com a plataforma. 
                             No caso da plataforma Windows, o default é o
                             Microsoft Visual C++. -->                                                                                                   
                        <cpp>                                                        
                            <name>g++</name>  
                        </cpp>
                        <!-- Idem para o linker -->             
                        <linker>
                            <name>g++</name>
                        </linker>
                        <!-- Definir o tipo de binário gerado, neste caso uma biblioteca estática -->
                        <libraries>
                            <library>
                                <type>static</type>
                            </library>
                        </libraries>                    
                    </configuration>                
                </plugin>
            </plugins>
        </build>
        
    </project>
    

    Alguns pontos devem ser observados aqui sobre o arquivo pom.xml:
    1. A configuração de packaging deve ser alterada de JAR para NAR (em letras minúsculas lá no XML), caso contrário o plugin não funcionará;
    2. A localização do repositório do plugin deve ser especificada; desde dezembro de 2013, o plugin tem sido armazenado no Sonatype OSS.
    3. O compilador padrão do plugin é de acordo com a plataforma: para Windows, o padrão é o Microsoft Visual C++ e portanto, caso o exemplo seja utilizado com este compilador, não é necessário definir o nome. Como o exemplo aqui está usando o Mingw, temos que definir o nome do compilador e linker como mostra a configuração no arquivo XML.
    4. Determinar o tipo de arquivo binário gerado, nesta caso uma biblioteca estática.
    Ainda, vale a pena citar a estrutura de arquivos que o plugin espera: por convenção, os arquivos fontes devem estar na pasta \src\main\c++ e os arquivos *.H que acompanham a biblioteca estática (e usados pela biblioteca dinâmica) devem para estar na pasta \src\main\include. É possível modificar ou acrescentar novas pastas. Consulte as opções de configuração oferecidas pelo plugin no site do projeto.

    Uma vez a compilação ocorra com sucesso, o plugin compacta a biblioteca estática e todos os arquivos contidos na pasta \src\main\include do projeto num único arquivo com a extensão NAR e os instala no repositório maven local.

    Projeto Biblioteca Dinâmica

    O arquivo pom.xml deste projeto é basicamente o mesmo do projeto anterior, com alteração no tipo de binário gerado e incluindo a dependência do projeto anterior para 'linkar'.

    <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>prototype</groupId>
        <artifactId>shared-project</artifactId>
        <version>1.0-SNAPSHOT</version>
      
        <packaging>nar</packaging>
    
        <name>shared-project</name>
    
        <!-- Repositório onde está armazenado o plugin -->
        <pluginRepositories>
            <pluginRepository>
                <id>Sonatype OSS</id>
                <url>http://oss.sonatype.org/content/groups/public</url>
            </pluginRepository>
        </pluginRepositories>
    
        <dependencies>
            <!--  dependência do projeto biblioteca estática  -->
            <dependency>           
                <groupId>prototype</groupId>
                <artifactId>lib-project</artifactId>
                <version>1.0-SNAPSHOT</version>
                <!-- Importante definir o tipo como NAR -->
                <type>nar</type>
            </dependency>
        </dependencies>
      
        <build>
            <plugins>            
                <plugin>
                    <groupId>com.github.maven-nar</groupId>
                    <artifactId>nar-maven-plugin</artifactId>
                    <version>3.0.0</version>
                    <extensions>true</extensions>
                    <configuration>                                                
                        <cpp>                                                        
                            <name>g++</name>  
                        </cpp>             
                        <linker>
                            <name>g++</name>
                        </linker>
                        <!-- Gera binário como biblioteca dinâmica -->
                        <libraries>
                            <library>
                                <type>shared</type>
                            </library>
                        </libraries>                    
                    </configuration>                
                </plugin>
            </plugins>
        </build>
        
    </project>
    

    Note que os arquivos *.H da biblioteca estática não precisam ser colocados explicitamente no projeto, conforme explicado anteriormente. Ao compilar um projeto que use esta dependência, o plugin descompacta o arquivo NAR da biblioteca estática e automaticamente inclui os arquivos *.H no caminho de pesquisa para o compilador. O mesmo acontece com o binário da biblioteca estática usado pelo linker.

    Considerações Finais

    A configuração do arquivo pom.xml para compilar código em C++ é muito simples com o nar-maven-plugin.

    O nar-maven-plugin recentemente voltou a ser mantido por um grupo de desenvolvedores e seu mailing list está ativo.

    A utilização do plugin é mais interessante quando se tem código Java que faz acesso código em C++ (ou em C) via JNI; o plugin é capaz de gerar automaticamente os headers e compilar juntos tanto o código Java quando o código em C++. Vou apresentar uma exemplo no próximo artigo.

    segunda-feira, 2 de julho de 2012

    Configurando Launch4J para Maven

    Launch4J é um aplicativo famoso usado para transformar arquivos JAR em arquivos executáveis para o sistema operacional Windows. É natural que se queira utilizar este aplicativo em conjunto com o Maven para gerar os arquivos finais do projeto para distribuição. Neste cenário, a intenção é atualizar sempre o número da versão e build do aplicativo automaticamente, antes mesmo de gerar o arquivo executável.

    A primeira opção para rodar o Launch4J integrado ao Maven é a utilização de plugin. Nos links de referência, no final deste artigo, estão listados alguns websites para o plugin Launch4J para Maven e outros links com exemplos de uso.

    A utilização do plugin pode ser mais limitada (ainda não testei) e ainda talvez se deseja utilizar a configuração já existentes do Launch4J do projeto, sem correr o risco de perder alguma configuração; este motivo justificaria a utilização do Launch4J era como um tarefa do Ant.  Não vou ensinar como criar este arquivo de configuração, pois já está razoavelmente documentado no próprio website do Launch4J. Assim, com um mínimo de adaptação, o Launch4J fica pronto para o Maven, como uma tarefa do Ant, como mostra o código abaixo:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <executions>
            <execution>
                <id>launch4j</id>
                <phase>package</phase>
                <goals>
                    <goal>run</goal>
                </goals>
                <configuration>
                    <tasks>                                
                        <copy file="config.xml" tofile="config-temp.xml"/>                                
                        <replace file="config-temp.xml" token="0.0.0.0" value="${project.version}.${BUILD_NUMBER}"/>
                        <taskdef name="launch4j" classname="net.sf.launch4j.ant.Launch4jTask" classpath="${launch4j.dir}/launch4j.jar:${launch4j.dir}/lib/xstream.jar" />
                        <launch4j configFile="./launch4j-client-temp.xml" jar="${project.build.directory}\my-application-${project.version}.jar" outfile="${project.build.directory}\my-application.exe"/>
                        <delete file="${project.build.directory}\my-application-${project.version}.jar"/> 
                        <delete file="config-temp.xml"/>
                    </tasks>
                </configuration>
            </execution>
        </executions>
    </plugin> 
    

    Aqui, utilizei o plug-in Antrun.  Da linha 13 à linha 18 estão as configurações das tarefas Ant para execução.

    A linha 13 copia o arquivo de configuração do Launch4J para um arquivo temporário, que é manipulado na sequência (linha 14). Neste arquivo, todas as sequências de caracteres 0.0.0.0 serão substituídas pelo número da versão do projeto (variável do próprio Maven: ${project.version}) e o número da build (variável do Hudson: ${BUILD_NUMBER}). Só lembrando, caso não esteja trabalhando com o Hudson, você deve remover esta última variável da composição dos números da versão do seu aplicativo.

    A definição da tarefa do Launch4J é feita na linha 15. Pressupõe-se que o aplicativo esteja instalado no ambiente de execução do Maven. Aqui, a variável ${launch4j.dir} fora definida anteriormente no arquivo pom.xml do projeto com o caminho da instalação do aplicativo. Observe que tal variável aparece duas vezes no atributo 'classpath' desta linha.

    Na sequência, linha 16, vem a chamada da própria tarefa definida da linha anterior. Esta tarefa demanda 2 atributos, sendo o nome do arquivo JAR e o nome do executável que será gerado. As variáveis utilizadas aqui são do próprio Maven.

    As linhas 17 e 18 fazem uma limpeza: remove o arquivo JAR, que agora já está encapsulado como um arquivo executável, e depois remove o temporário criado na linha 13.

    Conclusão

    A solução apresentada para se trabalhar com Launch4J e Maven atende aqueles que estão começando a trabalhar tanto com um ou outro destes aplicativos, sem exigir muita configuração por parte Maven. O plugin Antrun utilizado para fazer toda a configuração é da Apache e portanto se espera uma compatibilidade com as futuras versões do próprio Maven, facilitando assim uma possível atualização de versão.

    Referências

    http://launch4j.sourceforge.net/
    http://ant.apache.org/
    http://9stmaryrd.com/tools/launch4j-maven-plugin/
    http://mvnrepository.com/artifact/com.akathist.maven.plugins.launch4j/launch4j-maven-plugin
    http://stackoverflow.com/questions/6060560/trying-to-integrate-launch4j-in-a-maven-project-using-alakai-plugin