Criando uma aplicação multimódulo com Maven

Criando uma aplicação multimódulo com Maven

Esse post é uma versão atualizada de outro post que fiz há um bom tempo atrás e está disponível aqui.


Uma entre as várias decisões que um desenvolvedor deve tomar ao construir uma aplicação em Java diz respeito à organização de seus arquivos fonte e suas configurações. Em certos casos o desenvolvedor opta por simplesmente criar um grande projeto contendo todas as configurações e as classes separadas em pacotes. Apesar dessa abordagem funcionar, conforme a aplicação cresce os arquivos de configuração também crescem e a estrutura de pacotes se torna mais complexa, dificultando a manutenção e a evolução da aplicação. Outro problema muito comum nessa abordagem diz respeito à divisão lógica de responsabilidades, já que todas as classes do projeto estão acessíveis em todos os pontos, um desenvolvedor pode de forma descuidada usar uma classe onde não deveria, como por exemplo uma classe contendo método utilitários para a camada de apresentação ser acessada por uma classe de Serviços.

Outra tentativa de separação da aplicação consiste em criar vários projetos que geram artefatos (arquivos JAR, por exemplo) independentes que são reutilizados em outros projetos para compor a aplicação final. Entre os problemas dessa abordagem está o fato de ser necessário compilar cada projeto na ordem correta de dependência, além de garantir que nenhum projeto ficará sem ser compilado, o que poderá causar diversos problemas que serão descobertos somente quando a aplicação for executada ou em certos casos só em produção.

Uma outra forma de tratar esse problema consiste em reorganizar a aplicação em subprojetos dependentes e garantir que eles sejam compilados na ordem correta das dependências. O Maven possui uma funcionalidade que ajuda exatamente a fazer isso, que é a configuração de aplicações multimódulo. Nessa configuração cada subprojeto é visto como um módulo e um arquivo POM definindo quais são esses módulos. A estrutura de diretórios é bem simples, contendo um diretório com um arquivo pom.xml, chamado de parent pom, e um subdiretório para cada módulo, cada um também contento o seu pom.xml com as suas configurações. O parent pom pode conter também declarações comuns para toda a aplicação, tais como dependências e plugins que são herdadas por cada módulo.

Para exemplificar como é essa organização multimódulo utilizaremos uma aplicação fictícia chamada Soundtrack. A Soundtrack pode ser acessada via Web e as informações a respeito das músicas são obtidas através de outras aplicações que expõem APIs na Web. O código fonte para essa aplicação está disponível aqui no Github.

Para construir a aplicação serão utilizados dois módulos:

  • soundtrack-servive: que contém a implementação das regras de negócio da aplicação
  • soundtrack-web: que é a interface Web através da qual os usuários executam as funcionalidades.

Para montar um projeto multimódulo, criaremos a seguinte estrutura de diretório:

soundtrack  
├── soundtrack-services
│   └── pom.xml
├── soundtrack-web
│   └── pom.xml
└── pom.xml

Cada módulo contém um arquivo pom.xml usado para fazer o seu build. Cada módulo também herdará as definições feitas no parent pom.

A seguir detalharemos cada um dos arquivos pom.xml.

soundtrack/pom.xml – Parent pom

O parent pom da aplicação Soundtrack é um pom normal e contém definições comuns para a aplicação como groupId, artifactId, packaging e version. Um detalhe é que o packaging deve ser definido como pom, indicando que ele não vai gerar nenhum artefato tipo jar ou war e sim que ele contém definições compartilhadas por todos os módulos da aplicação. Esse pom deve relacionar os módulos que compõem a aplicação através da tag modules. Além da definição dos módulos, também podem ser declaradas as dependências da aplicação, seus escopos e versões com a tag dependencyManagement (ou pluginManagement para plugins). Uma vez que essas dependências foram declaradas, cada módulo pode indicar quais dessas dessas dependências serão usadas por ele.

A listagem abaixo mostra o parent pom do Soundtrack.

<?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>com.thecodeinside</groupId>
  <artifactId>soundtrack</artifactId>
  <packaging>pom</packaging>
  <version>1.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <modules>
    <module>soundtrack-services</module>
    <module>soundtrack-web</module>
  </modules>

  <dependencyManagement>
    <dependencies>
      <dependency>
          <groupId>io.undertow</groupId>
          <artifactId>undertow-core</artifactId>
          <version>1.3.23.Final</version>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>  

Repare no pom.xml a declaração dos módulos usando a tag <modules> e também as dependências usadas na aplicação com a tag <dependencyManagement>. O fato de declararmos essas dependências não implica que todas elas serão usadas em todos módulos. Cada módulo deverá definir as suas próprias dependências, com a diferença de que eles não precisam informar a versão, pois essa já está definida no parent pom. Isso garante que todos os módulos terão as mesmas versões das bibliotecas que dependem.

soundtrack-services/pom.xml e soundtrack-web/pom.xml

Os arquivos pom.xml dos módulos definem aquilo que cada módulo precisa para ser compilado, testado e para gerar os artefatos necessários. Essa definição inclui também a dependência para outros módulos da aplicação. Como estamos tratando de submódulos, o arquivo pom de fazer referência para o parent pom utilizando a tag parent. Devido à essa referência os módulos não precisam definir o groupId e o version em seus arquivos pom.

O módulo soundtrack-services deve gerar como artefato um arquivo JAR contendo as classes de serviço. Assim, no pom.xml desse módulo deve ser configurado o packaging como sendo JAR. Como esse módulo depende somente do JUnit, deve ser criada uma entrada específica para essa dependência, sem mencionar as suas versões que, como falamos anteriormente, já foi definida no parent pom. O pom.xml do soundtrack-services fica assim:

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

  <parent>
    <groupId>com.thecodeinside</groupId>
    <artifactId>soundtrack</artifactId>
    <version>1.0</version>
  </parent>

  <artifactId>soundtrack-services</artifactId>
  <packaging>jar</packaging>

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

O pom.xml do módulo soundtrack-web é muito parecido com o do soundtrack-services, exceto pelo fato dele definir o packaging como war, uma dependência para o módulo soundtrack-services e para o JUnit. Note que se não for definida explicitamente a dependência para a commons-collections nenhuma classe desse JAR estará acessível no módulo.

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

  <parent>
    <groupId>com.thecodeinside</groupId>
    <artifactId>soundtrack</artifactId>
    <version>1.0</version>
  </parent>

  <artifactId>soundtrack-web</artifactId>
  <packaging>war</packaging>

  <dependencies>
    <dependency>
      <groupId>com.thecodeinside</groupId>
      <artifactId>soundtrack-services</artifactId>
      <version>${parent.version}</version>
    </dependency>
    <dependency>
        <groupId>io.undertow</groupId>
        <artifactId>undertow-core</artifactId>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  [...]

</project>  

Feitas as configurações da aplicação em módulos podemos executar a aplicação. Supondo que ${soundtrack-path} seja o diretório raiz da aplicação, utilize os seguintes comandos para compilar e executar a aplicação:

$ cd ${soundtrack-path}
$ mvn package
$ cd ./soundtrack-web
$ mvn exec:java

Se tudo correu bem, a aplicação estará em execução. Abra o browser e digite a seguinte URL: http://localhost:8080. Você deverá ver o seguinte resultado:

Rainbow in the Dark  
For Those About To Rock  
The Court of the Crimson King  
And Justice for All  
The Working Man  
Dread And The Fugitive Mind  
Heart of the Sunrise  
Children of the Grave  

Um último detalhe. O módulo soundtrack-web depende do soundtrack-services que deve ser compilado primeiro. No parent pom o soundtrack-services foi definido antes. E se essa declaração for invertida?

<modules>  
  <module>soundtrack-web</module>
  <module>soundtrack-services</module>
</modules>  

Bem, isso não afeta nada! O Maven é esperto o suficiente para verificar cada módulo, montar um grafo de dependência entre eles e fazer a compilação na ordem correta. Na verdade, existe um conjunto de regras para definir essa ordem e mais detalhes podem ser obtidos nesse link: Guide to Working with Multiple Modules.

Bons builds para todos.

Subscribe to