A few months ago I wrote an article about using Ivy to jump-start new Java applications. Since then I have used Ivy in all the new components which often have dependencies between themselves or third-party libraries sometimes open source, sometimes proprietary. This article is about new insights from this experience and the more general issue of application modularization.
Application modularization is a hot topic in the Java community. A concise and good article by Alex Blewitt is: Modular Java: What Is It?. It is easy to see the dependencies being a problem in large applications, but it affects small/medium scale applications as well. In these days it is easy to create a component/application that depends on a dozen of external libraries. Modules can depend on each other at compile-time and runtime. For compile-time dependencies there is no standard specification, instead ad-hoc formats have been defined by development tools: POM for Maven and Ivy files for Ivy. For run-time dependencies the current standard is OSGi implemented by the major J2EE application containers.
It is easy to see how a development team can be easily confused and frustrated by the topic of modularization. On one hand this is a common issue: new components are build and new versions are released by other teams and external parties. Compile-time dependencies are managed inside build scripts, and hard to maintain. For run-time dependencies, the standards (OSGi) and tools (Eclipse, bnd) are too complex to the point where only the tool providers are able to use them.
For the people who want to go beyond manually maintained classpath entries, I am suggesting to a combination of Ivy and jar MANIFEST files.
First, create the Ivy file with the dependencies:
<ivy-module version="2.0">
<info organisation="intspc" module="jump-start"/>
<configurations>
<conf name="runtime"/>
<conf name="compile" extends="runtime"/>
<conf name="test" extends="runtime"/>
</configurations>
<dependencies defaultconfmapping="*->*,!javadoc,!sources">
<dependency org="log4j" name="log4j" rev="1.2.15">
<exclude module="jms"/>
<exclude module="jmxtools"/>
<exclude module="jmxri"/>
<exclude module="mail"/>
</dependency>
<dependency org="junit" name="junit" rev="3.8.1" conf="test->runtime(default)"/>
</dependencies>
</ivy-module>
Note that I created three configurations: compile, test and runtime to distinguish in which phase of the build-test-run process is the dependency used. In this simple example my application is dependent on log4j at compile/runtime and on junit at test-time.
Next, initialize the ant properties for the different class paths:
<target name="ivy-resolve" description="--> retreive dependencies with ivy">
<ivy:settings file="${jlib.home}/ivy-settings/ivysettings.xml" />
<ivy:resolve conf="runtime"/>
<ivy:cachepath pathid="runtime.classpath" />
<echo message="Runtime classpath: ${toString:runtime.classpath}"/>
<ivy:resolve conf="compile"/>
<ivy:cachepath pathid="compile.classpath" />
<echo message="Compile classpath: ${toString:compile.classpath}"/>
<ivy:resolve conf="test"/>
<ivy:cachepath pathid="test.classpath" />
<echo message="Test classpath: ${toString:test.classpath}"/>
</target>
The usual targets, “compile” and “test” will use the path properties: compile.classpath and test.classpath. I will focus here on building the executable jar. The information about the class dependencies is stored in the MANIFEST file, which, in our case, has to include the log4j reference in the Class-Path entry:
Manifest-Version: 1.0 Ant-Version: Apache Ant 1.7.0 Created-By: 1.5.0_11-b03 (Sun Microsystems Inc.) Built-By: calin Main-Class: com.intspc.jmpst.HelloWorld Class-Path: ../../lib/log4j-1.2.15.jar Name: main Implementation-Title: jmpst Implementation-Version: 1.0.0
To create this MANIFEST, the ant task uses the ant-property runtime.classpath and the ant-task manifestclasspath:
<target name="build" depends="compile, get-build-number">
<manifestclasspath property="manifest.cp" jarfile="${jlib.home}/intspc/jars/${ant.project.name}.jar">
<classpath refid="runtime.classpath" />
</manifestclasspath>
<property name="manifest.file" value="${main.classes.dir}/build/${ant.project.name}.mf"/>
<manifest file="${manifest.file}">
<attribute name="Built-By" value="${user.name}" />
<attribute name="Main-Class" value="com.intspc.jmpst.HelloWorld" />
<attribute name="Class-Path" value="${manifest.cp}" />
<section name="main">
<attribute name="Implementation-Title" value="${ant.project.name}" />
<attribute name="Implementation-Version" value="${version}" />
</section>
</manifest>
<jar basedir="${main.classes.dir}" destfile="${lib.dir}/${ant.project.name}-${version}.jar"
manifest="${manifest.file}">
<include name="**/*" />
</jar>
</target>
As a last step, we can create a zip file with all the run-time dependencies. This helps in the distribution process:
<target name="build-pkg-deps" depends="ivy-resolve">
<echo message="Runtime classpath: ${toString:runtime.classpath}" />
<echo message="Ivy cache dir: ${ivy.cache.dir}" />
<pathconvert refid="runtime.classpath" property="pkg.runtime.files" pathsep=",">
<map from="${ivy.cache.dir}${file.separator}" to=""/>
</pathconvert>
<zip destfile="releases/${ant.project.name}-deps-${version}.zip"
basedir="${ivy.cache.dir}"
includes="${pkg.runtime.files}"/>
</target>
In conclusion, Ivy can be used to define dependencies between modules at compile, test and runtime. At compile and test time, Ant tasks use the Ivy information through derived ant-properties. For runtime, the Ivy dependencies are converted into Class-Path entries in the jar MANIFEST file. Finally, it is possible to generate a zip file with all the external dependencies.
No Comments
No comments yet.