Dependency Management with Apache Ivy
Java Code Dependencies
Two popular options in the dependency management space are Maven and Ant/Ivy. Both toolsets have their merits, but in this article we'll look at the Ant/Ivy combination.
Ant is a build tool and Ivy is a dependency management tool. Maven, on the other hand, is both a build tool and a dependency manager. There are pros and cons to using two tools as opposed to one. With just one tool, you have a kind of one-stop shop. However, the combined tool may be a little harder to use than is the case for the individual tools.
With Ant and Ivy, each tool is dedicated to doing just one thing. This approach can make them a little easier to understand, particularly when things go wrong. Also, the Ant/Ivy combination is a good example of the old UNIX principle of doing one thing and doing it well: Ant is a solid build tool, and Ivy is likewise a reliable dependency manager.
Because of their close relationship, Ivy even comes with a set of pre-built Ant tasks to help you get started using Ivy. Before we get into the details of how to work with Ant/Ivy, let's look a little at the area of dependency management in general.
Rationale for Dependency Automation
How often have you struggled with Eclipse or some other IDE, trying to get rid of the compilation error markers in your Java files? This problem is often caused by missing (or incorrect) dependency errors. A little later in this article, we'll look at a concrete Eclipse example that illustrates the key role automatic dependency management can play in fixing these knotty issues. We'll also examine the workflow for using Internet repositories, such as Maven Central.
Managing code dependencies generally boils down to a simple choice: manual or automatic. If, like me, you prefer to exert a lot of control over your code dependencies, then the manual path is attractive, at least initially. However, a point tends to come when dependency management really needs to be automated.
When you think about it, complex dependency issues don't have a lot to do with design and coding, so it's natural to think about using a tool for this often tedious task.
As your code grows, it acquires what's called a tree of dependencies. Let's look at this next.
The Tree of Dependencies
Any Java project of reasonable complexity has a non-trivial set of dependencies consisting of JAR files with the internal resources needed to build, deploy, and run an application. Sounds simple, but these dependencies can get out of hand. Let's explore this with an example.
Adding a New Facility to Your Code
Let's say, for example, you read my earlier article "Java Data Validation Using Hibernate Validator," and you've decided to add a third-party validation mechanism to your application. Validation is often added late in a project, and very often programmers will opt to create their own bespoke solutions. But let's assume you decide to go with an open source solution.
One decent choice in this space is Hibernate Validator. To make this a little more concrete, let's assume that you already have an existing Ant build file. Adding Hibernate Validator then amounts to nothing more than installing Ivy and the addition of a single Ant target to fetch the dependencies. It's a similar story if you use Maven for dependency management; you just make a small addition to your metadata file pom.xml. Because I used Maven in the previous article, we can compare the two approaches.
Listing 1 illustrates the required Ivy setup for fetching the dependencies from the Maven Central Repository.
Listing 1Adding an Ivy dependency.
<target name="maven2-namespace-deps-validator" depends="init-ivy" description="-->
install module with dependencies from maven2 repo using namespaces"> <ivy:install settingsRef="advanced.settings" organisation="hibernate"
module="hibernate-validator" revision="5.2.2.Final" from="${from.resolver}"
to="${to.resolver}" transitive="true"/> </target>
Don't worry about the details in Listing 1 for the moment. Listing 1 is basically like a little program that exists to fulfill our required dependency. The key part is the following section:
module="hibernate-validator" revision="5.2.2.Final" from="${from.resolver}" to="${to.resolver}" transitive="true"/>
This line specifies that we want to install a version of a given artifact; in this case, Hibernate Validator version 5.2.2.Final. Also, we want to use a specific source repository (from.resolver) and install the module in our destination repository (to.resolver). The two repositories are specified as Ant properties; in this case, respectively, Maven Central Repository (from) and a local file-based Ivy repository (to).
Transitive Dependencies
The potentially scary part in the above line is the setting for the transitive property. What are transitive dependencies? Well, transitive is a mathematical term that just means if module A has a dependency on module B, then both modules A and B will be downloaded. In other words, the transitive relationship is inferred by Ivy, and the appropriate artifacts are acquired automatically. Clearly, module B can also depend on C, and so on.
Once transitive dependencies are specified in this way, then all related artifacts will be downloaded for you. Setting transitive to true means that all our required dependencies will be downloaded. Sounds innocent, doesn't it? So what happens when we run this target? Well, we get a big bunch of files added to the local file-based repository, as illustrated in Figure 1.
Figure 1 Our new set of dependencies.
Figure 1 illustrates the outermost folder for each downloaded dependency. The longwinded point of this discussion is that the decision to add the required Hibernate Validator artifact is not without costs. What might those costs be?
For one thing, the deployed application now has to include these dependent modules. This requires disk space. At runtime, as the resources in the dependencies are used, there will be an associated memory requirement.
Depending on your deployment environment, some of these dependencies might already be available; for example, in a JEE scenario. However, if you're running a JSE application, you might well need all of the dependencies in Figure 1.
Clearly, automatic dependency management is a very powerful tool!
The sudden increase in dependencies that can result from adding an artifact such as Hibernate Validator can strike fear into the hearts of project team leaders. The resulting potentially complex web of dependencies is also, in a sense, an expression of how far open source development has come. So many useful utilities are available that you can simply add them to your code instead of developing them from scratch. The flipside is that such code may drag in other unwanted dependencies.
I recently read that something like 88% of all code (some 5 billion dollars' worth [1]) is now available as existing open source. In this context, the job of the programmer is often one of configuring existing tools and frameworks, rather than writing lots of new code.
It's important to be careful with the dependencies you add. Adding required artifacts to your Ant or Maven metadata files might be simple, but it also might result in a spaghetti of unnecessary dependencies. On the other hand, writing your own validation code also has issues. Skillful dependency management is a complex balancing act.
Dependency Bloat and Versioning
A less obvious burden in Figure 1 is the future need to manage the versioning of the dependencies. This issue is usually seen in codebases that have been around for a few years, where programmers use a given version of a library, such as log4j. Later, another programmer comes along and uses an updated version of log4j. Sadly, our second programmer doesn't update or delete the earlier code and its dependency.
The result is that we are now lumbered with two dependencies for the price of one. The underlying code may also be unnecessarily bloated, and without rules for handling this type of problem, the situation is only likely to get worse.
If you decide to add a new dependency, it's always a good habit to check whether any older dependencies can be retired. This might require some code changes if a programmer has written against a specific version of a library.
Ivy and Maven Port Use
Many organizations disallow the use of Internet code repositories, and with good reason. Maven Central passes back binary files to clients, which is not without risk. One way to ameliorate—but not eliminate—the risk of binary file download is by using digital signatures. Going back to Figure 1, if we double-click three times into the apache folder, this brings us to the actual JAR file and the digital signatures in Figure 2.
Figure 2 The artifact folder with the digital signatures.
Notice the signature files in Figure 2. This allows us to verify that the binary log4j JAR file matches the MD5 and SHA1 signature files. The presence of signature files doesn't guarantee that the files have not been tampered with, but it's one safeguard.
Running a Java Project After Dependency Acquisition
Let's create a simple Java project that requires the Hibernate Validator dependencies we downloaded earlier.
Listing 2 illustrates an example of a Hibernate Validator use case in a simple domain entity class. The example code is based on that at the Hibernate Validator site.
Listing 2An entity domain class.
public class DomainClass { @NotNull private String manufacturer; @NotNull @Size(min = 2, max = 14) private String licensePlate; @Min(2) private int seatCount; public DomainClass(String manufacturer, String licensePlate, int seatCount) { this.manufacturer = manufacturer; this.licensePlate = licensePlate; this.seatCount = seatCount; } public static void main(String[] args) { DomainClass domainObject = new DomainClass(null, null, 10); ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<DomainClass>> constraintViolations = validator.validate(domainObject); assertEquals(2, constraintViolations.size()); assertEquals("may not be null", constraintViolations.iterator().next().getMessage()); } }
If we simply download the Hibernate Validator JAR file and add it to the Eclipse project build path, we'll run into a rather unfriendly exception such as the one in Listing 3.
Listing 3Dependency-related exception.
Exception in thread "main" java.lang.NoClassDefFoundError: javax/validation/ParameterNameProvider at org.hibernate.validator.HibernateValidator.createGenericConfiguration(HibernateValidator.java:41) at javax.validation.Validation$GenericBootstrapImpl.configure(Validation.java:269) at javax.validation.Validation.buildDefaultValidatorFactory(Validation.java:111) at validator.DomainClass.main(DomainClass.java:37) Caused by: java.lang.ClassNotFoundException: javax.validation.ParameterNameProvider at java.net.URLClassLoader$1.run(Unknown Source) at java.net.URLClassLoader$1.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) ... 4 more
Fixing this issue is a real pain if you opt for manual dependency management. This exception can be resolved by using Maven, or, as in our case, Apache Ivy. Once the dependencies have been downloaded as in Figure 1, we can then update the Eclipse build path and re-run the application. After applying the dependencies, we should see a successful application run, as illustrated in Listing 4.
Listing 4A successful run.
Exception in thread "main" java.lang.AssertionError: expected:<1> but was:<2> at org.junit.Assert.fail(Assert.java:93) at org.junit.Assert.failNotEquals(Assert.java:647) at org.junit.Assert.assertEquals(Assert.java:128) at org.junit.Assert.assertEquals(Assert.java:472) at org.junit.Assert.assertEquals(Assert.java:456) at validator.DomainClass.main(DomainClass.java:42)
Programming Without Automatic Dependency Management
Though it can be very tiresome chasing down dependencies manually, this model is still used by many organizations. Unfortunately, the time you spend solving transitive dependency issues is time taken away from design and coding. By contrast, getting set up with Ivy has a once-only time cost, and thereafter dependency management is handled automatically.
Building a Simple Ivy Setup
The first step is to download Ivy. I won't duplicate the excellent content on the Ivy site. Getting up and running with Ivy isn't too difficult. A simple ivy.xml file such as the following is sufficient to download two artifacts (Commons lang and Commons cli, respectively) from Maven Central:
<ivy-module version="2.0"> <info organisation="apache" module="hello-ivy"/> <dependencies> <dependency org="commons-lang" name="commons-lang" rev="2.0"/> <dependency org="commons-cli" name="commons-cli" rev="1.0"/> </dependencies> </ivy-module>
A key required technique in Ivy.Maven dependency management is learning to use Maven Central. Let's have a look at this now.
Using the Maven Central Repository
Let's say you want to locate a given artifact, such as the Hibernate Validator, using Maven Central. The first step is to visit the Search Engine for the Central Repository. Next, type in the required artifact name, and you should see something like the excerpt in Figure 3.
Figure 3 Maven Central.
Click the generic link for 5.2.2.Final to the right of hibernate-validator-parent under "Latest Version." (The other links relate to OSGI artifacts—a somewhat more specialized area.) This brings us to another screen, illustrated in Figure 4.
Figure 4 Artifact details.
In Figure 4, notice the links under the heading "Dependency Information." This really useful part tells you what metadata to specify in order to acquire the artifact automatically. The metadata is supplied for POM (Maven), Ivy, etc. You select the Ivy setting, copy the metadata, and add it to your Ivy setup. In this case, you would copy the following line:
<dependency org="org.hibernate" name="hibernate-validator-parent" rev="5.2.2.Final" />
Just drop this dependency into your ivy.xml file, run ant, and the set of artifacts will be downloaded. It's that simple.
Other Dependency Management Tools
Maven and Ivy are just two among a range of popular dependency management tools. Gradle is another, referred to as a polyglot build tool. I haven't used Gradle, but it seems to be well suited to multiple-language environments.
Conclusion
Build tools and dependency management tools are closely related. Maven is an example of both. Ant and Ivy are separate tools—one for builds and the other for dependencies.
As codebases become larger and more complex, using a dependency manager is customary. In fact, it's usually advisable, as chasing down dependencies can become tiresome. This type of problem is seen when you add a new open source facility such as Hibernate Validator to your codebase. The required dependencies can be surprisingly large.
Provided that you're happy with the tree of dependencies, you can employ Ivy to fix some pretty knotty dependency problems, which sometimes are nearly too complex to fix by hand.
Getting set up with Ivy is straightforward, and if your organization allows Maven Central access, your migration should be relatively painless.
A key skill in using either Ivy or Maven is figuring out the metadata for your required dependencies. The Maven Central Repository provides easy tools for this task. Extracting a given dependency usually amounts to no more than copying a line of metadata into your local ivy.xml file.
It's important to note that the dependency management arena has a good few competitors. For example, Gradle is good for multi-language environments. It's not a one-horse race!
References
[1] Linux Format reports in the November 2015 issue that the total value of the Linux Foundation Collaborative Projects is estimated at approximately 5 billion dollars.