Improving Java Software Builds with Maven - Part 2

by Robert Shanahan

Continued from Improving Java Software Builds with Maven - Part 1

Dependency Management

Scope

Dependency scope is used to limit the transitivity of a dependency, and also to affect the classpath used for various build tasks. There are 5 scopes available:

Compile - this is the default scope, used if none is specified. Compile dependencies are available in all classpaths.

Provided - this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.

Runtime - this scope indicates that the dependency is not required for compilation, but is for execution. It is in the runtime and test classpaths, but not the compile classpath.

Test - this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases.

System - this scope is similar to provided except that you have to provide the JAR which contains it explicitly. The artifact is always available and is not looked up in a repository.

Ear with War(s)

With respect to JEE ear packaging, Maven dependency management is sorely lacking. There is no clear cut way to tell Maven that a webapp dependency is provided by its containing ear. Using provided scope results in no manifest entry in the webapp's META-INF/manifest.mf file. Configuring a dependency as true will prevent a dependency from being included in the webapp's WEB-INF/lib directory and it will include it in the manifest. So, there you have it, right? Wrong! Unfortunately the optional configuration doesn’t prevent inclusion of all of the transitive dependencies of an optional dependency, which will end up in the WEB-INF/lib directory of the webapp's war file. Using the optional config option doesn't feel right anyway. The correct solution is to define a new scope called ear that would honor the JEE ear packaging specification. We can hope Maven 2.1 will plug this blatant hole. In the mean time, I present an ugly workaround:

The ugliest aspect of this workaround is that if you specify a classpathPrefix (common to put 3rd party jars in lib directory of ear) for the manifest configuration, the prefix is also added to jars that will be packaged with the war in WEB-INF/lib. Fortunately the classloader correctly resolves all jars stored in WEB-INF/lib regardless of war manifest specification.

Multi-module Projects

Maven supports multi-module and hierarchical projects. The most common example of a multi-module project is a JEE project with an ear file containing zero or more ejbs, one or more wars and zero or more utility jars.
A typical ear structure:

ear
 +lib
  utility1.jar
  utility2.jar
  3rdparty1.jar
  3rdparty2.jar
 +war1
 +WEB-INF
 +lib
  web1_1.jar
  web1_2.jar
 +war2
 +WEB-INF
 +lib
  web2_1.jar
  web2_2.jar
 ejb1.jar
 ejb2.jar

A corresponding multi-module Maven project would be structured like this:

JEE Project
 ear
 web1
 web2
 ejb1
 ejb2
 utility

Such a project has a top level pom.xml, which serves as the parent to each of the sub-module poms. The parent pom must explicitly identify each sub-module.

<modules>
 <module>utility</module>
 <module>ejb1</module>
 <module>ejb2</module>
 <module>web1</module>
 <module>web2</module>
 <module>ear</module>
</modules>

dependencyManagement Tag

With few exceptions, each sub-module will depend on the same version of a shared dependency. To this end, Maven supports centralized management of dependency versions with the dependencyManagement tag, which simply specifies the version of each common/shared dependency. A top level pom:

<dependencyManagement>
<dependencies>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring</artifactId>
 <version>2.5.5</version>
</dependency>
<dependencies>
</dependencyManagement>

For dependencies specified in the parent pom, a sub-module needn't specify version, which obviously simplifies the task of changing dependency versions.

<dependencies>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring</artifactId>
</dependency>
<dependency>

Deployment

With the basic dependency management in place, the next challenge is getting jars to deploy to the appropriate directory within the ear. Ejb modules, according to JEE convention (and our defined ear layout), go in the root directory of the ear and require an entry in the ear/META-INF/application.xml file. Wars likewise reside in the ear root directory and have entries in application.xml.

ear module pom.xml excerpt:

...
<dependencies>
 <dependency>
  <groupId>ejb1</groupId>
  <artifactId>ejb1</artifactId>
  <version>${parent.version}</version>
  <type>ejb</type>
 </dependency>
 <dependency>
  <groupId>ejb2</groupId>
  <artifactId>ejb2</artifactId>
  <version>${parent.version}</version>
  <type>ejb</type>
 </dependency>
 <dependency>
  <groupId>web1</groupId>
  <artifactId>web1</artifactId>
  <version>${parent.version}</version>
  <type>war</type>
 </dependency>
 <dependency>
  <groupId>web2</groupId>
  <artifactId>web2</artifactId>
  <version>${parent.version}</version>
  <type>war</type>
 </dependency>
 ...
</dependencies>
...
 <build>
  <finalName>sample</finalName>
  <plugins>
   <plugin>
    <artifactId>maven-ear-plugin</artifactId>
    <configuration>
      <displayName>sample</displayName>
      <description>sample</description>
      <defaultLibBundleDir>lib/</defaultLibBundleDir>
      <modules>
       <!-- only necessary if different bundleDir desired
       <ejbModule>
        <groupId>ejb1</groupId>
        <artifactId>ejb1</artifactId>
        <bundleDir>/</bundleDir>
       </ejbModule>
       -->
       <webModule>
        <groupId>web1</groupId>
        <artifactId>web1</artifactId>
        <contextRoot>/web-app1</contextRoot>
        <bundleDir>/</bundleDir>
       </webModule>
       <webModule>
        <groupId>web2</groupId>
        <artifactId>web2</artifactId>
        <contextRoot>/web-app2</contextRoot>
        <bundleDir>/</bundleDir>
       </webModule>
      </modules>
      <archive>
       <manifest>
        <addClasspath>true</addClasspath>
        <classpathPrefix>lib/</classpathPrefix>
       </manifest>
      </archive>
    </configuration>
   </plugin>
  </plugins>
 </build>

We discussed previously a hack-around to configure war dependencies provided by an ear (Ear with War(s)). It turns out there is a much simpler technique; however the technique requires that the target app server honors a war/META-INF/manifest.mf classpath specification that consists of only a directory name.
The war pom.xml excerpt:

...
 <build>
  <finalName>${project.artifactId}</finalName>
  <plugins>
   <plugin>
    <artifactId>maven-war-plugin</artifactId>
    <version>2.1-alpha-1</version>
    <configuration>
     <archive>
      <manifestEntries>
       <Class-Path>
        lib/
       </Class-Path>
      </manifestEntries>
     </archive>
    </configuration>
   </plugin>
  </plugins>
 </build>
 <dependencies>
  <!-- The following 2 dependencies will be bundled
		in the war's WEB-INF/lib directory-->
  <dependency>
   <groupId>com.ve.kavachart</groupId>
   <artifactId>kcServlet</artifactId>
   <version>5.3.0b</version>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.apache.struts</groupId>
   <artifactId>struts2-core</artifactId>
   <version>2.0.11.2</version>
   <scope>compile</scope>
  </dependency>
  <!-- The following 2 dependencies must be added to the
       ear's set of dependencies with 'compile' scope-->
  <dependency>
   <groupId>commons-fileupload</groupId>
   <artifactId>commons-fileupload</artifactId>
   <version>1.0</version>
   <scope>provided</scope>
  </dependency>
  <dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
   <scope>provided</scope>
  </dependency>
 </dependencies>
...

JBoss 4.2.x and WebSphere 6.1.x both support this technique. Geronimo 2.x does not.

Maven and Ant

Maven supports Ant integration via the AntRun plugin. The intent of this plugin is to facilitate migration from Ant to Maven and Ant tasks should only be used where Maven functionality is yet lacking. One such example is the Ant sshexec task, which has no Maven equivalent. The following will deploy (scp) an ear a remote host.

<!-- This goes in the root project tag -->
<build>
 <plugins>
  <plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-antrun-plugin</artifactId>
   <dependencies>
    <dependency>
     <groupId>ant</groupId>
     <artifactId>ant-optional</artifactId>
     <version>1.5.3-1</version>
    </dependency>
    <dependency>
     <!-- required for scp task -->
     <groupId>ant</groupId>
     <artifactId>ant-jsch</artifactId>
     <version>1.6.5</version>
    </dependency>
   </dependencies>
   <executions>
    <execution>
     <phase>package</phase>
     <configuration>
      <!-- put the Ant Tasks that you wish to call here -->
      <tasks if="optionalConditional">
       <scp todir="jboss@fluorine:/usr/local/jboss/
                   jboss-4.2.1.GA/server/default/deploy/"
            trust="true" password="jboss"
            file="${project.build.directory}
            /${project.build.finalName}.${project.packaging}" />
       <echo>Deploy complete.</echo>
      </tasks>
     </configuration>
     <goals>
      <goal>run</goal>
     </goals>
    </execution>
   </executions>
  </plugin>
 </plugins>
</build>

Alternatively, use existing build.xml:

<build>
 <plugins>
  <plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-antrun-plugin</artifactId>
   <executions>
    <execution>
     <phase>deploy</phase>
     <configuration>
      <!-- put the Ant Tasks that you wish to call here -->
      <tasks>
       <ant antfile="build.xml" target="sometarget"/>
      </tasks>
     </configuration>
     <goals>
      <goal>run</goal>
     </goals>
    </execution>
   </executions>
  </plugin>
 </plugins>
</build>
info@stoutsystems.com
877.663.0877
© Copyright 1995-2022 - STOUT SYSTEMS DEVELOPMENT INC. - All Rights Reserved
envelopephone-handsetlaptop linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram