Continued from Improving Java Software Builds with Maven - Part 1
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.
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.
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>
dependencyManagementTag
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>
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 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>