ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

An Ant Modular Build Environment for Enterprise Applications

by Les A. Hazlewood
06/22/2005

The build environments for today's Java enterprise applications are becoming harder and harder to manage. Large amounts of code, configuration files, and third-party dependencies make organizing these builds difficult.

In a simple world, we would only ever have to lump all of our source code under one root directory, all config files in another, and third-party libraries in another. But enterprise environments rarely follow the simple path. Today's enterprise Java projects are complex in structure, functionality, and organization. They usually have a lot of source code and supporting artifacts (properties files, images, etc.) to manage. With so much to organize, teams often find themselves confused and frustrated when trying to set up an optimal build solution.

Wouldn't it be nice if our build environment could cleanly handle all of our source code in a unified structure, regardless of the project's size?

This article shows one such example of an Ant build environment that has been modified from experience with many projects over the years. It may not be the best environment out there, but it has certainly stood the test of time and will help you get up and running very quickly on almost any project, tiny or huge.

Caveats

Just some notes of warning first, so you don't read this article and find it of little value:
  • This article assumes working knowledge of Ant. It is intended for people that are competent and comfortable in existing Ant environments.
  • The build environment defined herein is modular, and modules are defined by directories and subdirectories. That means files and source code are stored in many different directory locations. As such, this article is most useful if you use an IDE, like Eclipse or IntelliJ Idea, that can manage class and file locations for you. You can use a text editor, too, but you may find yourself traversing multiple directory trees more often than you like.

Concepts

Let's first cover the core concepts behind this build environment. We will say it is modular, hierarchical and artifact-driven. But what does this mean exactly?

Modular

A modular build is one that is organized around software modules. A module is a logically aggregated unit of functionality that corresponds to a named feature in a system. In the build environment itself, a module is represented as a self-contained collection of source code and config files used to build a software artifact representing that named feature. It almost always has a one-to-one correspondence with a directory tree in your Revision Control System (RCS) such as CVS or Subversion. Examples of a module could be security, administration, wiki, email, etc.

Related Reading

Ant: The Definitive Guide
By Steve Holzner

Hierarchical

A hierarchical build is one that has a hierarchy of modules. That is, it is possible for a module to be composed of smaller, more specific child modules called submodules.

If a module has children, it is responsible for ensuring those children modules are built in the proper manner. Later, we'll discuss how the example build environment applies the hierarchical concept.

Artifact-Driven

An artifact-driven build is one where each module or submodule exists for the purpose of generating a single deployable artifact. In Java projects, these artifacts are almost always .jar, .war, or .ear files. In other types of builds, they are usually binary executables or dynamically linked libraries (.dll or .so).

The example build environment is also artifact-driven, and we'll discuss how it creates deployable artifacts.

Although these three concepts are pretty easy to understand, they become very powerful when incorporated into a build environment.

Now let's take a first look into how the environment is organized.

Modular Organization

When there is a lot to accomplish, it makes sense to break down the problem into smaller parts. We need a good divide-and-conquer technique to help manage the large amounts of source code. It makes sense to do this in a build environment by creating build modules.

We create a module by creating a directory under the application root. This new directory becomes the module's base. Under each module directory, we find all the files and source code related to that module.

Here is a sample application's build environment, organized in modules:


  appname/
  |-- admin/
  |-- core/
  |-- db/
  |-- lib/
  |-- ordermgt/
  |-- reports/
  |-- web/
  |-- build.xml
  

And here's what each entry means:

  • Each directory except for lib/ is a module. In this sample environment, we have an admin module that provides implementations of business POJOs that allow someone to administer the application (e.g., create users, assign permissions, etc.). Likewise, there is a reports module, which is where we can find the implementations of components that enable report generation. The core module is sort of a catch-all module for components that are used across any/all modules and can't really be associated with just one system function (e.g. StringUtil classes, etc.). Typically, all other modules will depend upon the core module.

    The other modules are just like the admin, reports, and core modules: they each deal with a respective system function that is mostly self-contained and different from any other module. Also, since our sample app can support web-based interaction, we also have a web module, which includes everything needed to build a .war file.

  • The lib/ directory is a little special. It contains all third-party .jars required to either build or run the application. We keep all third-party .jars used by our modules in this directory, instead of in the modules themselves, for three reasons:

    1. It's easier to manage third-party dependencies from a single location. Whether or not a module uses one of these libraries is defined in a module-specific Ant <path> entry in the module's build.xml file.

    2. It avoids classloading or API version conflicts by eliminating the possibility of duplicate .jars. If more than one module uses Jakarta Commons Logging, which module is responsible for storing the commons-logging.jar file? If each stored their own copy, there is the potential that one module might have one version and another module might have a different version. When the application is being run, only the first .jar file found in the classpath is used to satisfy the dependency, potentially causing a conflict for the other module. We avoid that by managing only one .jar at the root level.

    3. Third-party dependencies are versioned with your source code. Often overlooked in many projects, this is the most important reason why you want to store your dependency libraries in a RCS. By doing this, you ensure that no matter what version or branch of your software you check out, you'll always have the proper versions of third-party libraries needed to run that particular version of your software.

  • The root build.xml file is primarily just a management file. It is responsible for knowing what build files and targets are necessary to build each module. The module then does what it needs to ensure its artifact is built properly.

    For example, if the project was being built, and it was time to build the ordermgt module, the root build file would "know" to call an Ant task in the ordermgt/build.xml file. The ordermgt/build.xml file would then know exactly what is required to create the ordermgt .jar file. Also, if this project could be built and entirely consolidated into a .ear file, this build.xml file would be responsible for building that .ear.

How does the root build.xml file know to build the modules and the order in which they are to be built for any given target? Here's a snippet of Ant XML that shows how:

<!-- =========================================
     Template target.  Never called explicitly, 
     only used to pass calls to underlying 
     children modules.
     ========================================= --> 
<target name="template" depends="init">
    <-- Define the modules and the order in which 
        they are executed for any given target.  
        This means _order matters_.  Any 
        dependencies that are to be satisfied by
        one module for another must be declared 
        in the order the dependencies occur. -->
    <echo>Executing &quot;${target}&quot; \ 
             target for the core module...</echo>
    <ant target="${target}" dir="core"/>
    <echo>Executing &quot;${target}&quot; \
            target for the admin module...</echo>
    <ant target="${target}" dir="admin"/>
    ...
</target>

This template target passes on whatever build target is called on this root build.xml file to the children modules in a known order. For example, if we wanted to clean the entire project, you would only have to call the clean target at the root of the project, and the following task is executed:

<!-- =========================================
     Clean all modules.
     ========================================= -->
<target name="clean" depends="init">
    <echo>Cleaning all builds"</echo>
    <antcall target="template">
        <param name="target" value="clean"/>
    </antcall>
</target>

This root clean target is explicitly called and the build.xml file in turn implicitly calls the template target, which ensures that all modules are cleaned.

The above modular organization and related build targets really makes managing source code and builds easier. The structure helps you find code you want to work with faster and more easily. And the template target organizes how things are executed.

But here's the best part of the modular structure:

After doing a full build on the whole project, any module can be built independently of the full build. Just change in to the module directory on the command line and run:

> ant target
and that module's build.xml file takes over. You can run any target at any level in the build, and only that level will be built.

Why is this important? Because it allows you to work independently in your module space and build just that module. Each change you make to a module's source file doesn't require you to build the entire project all over again. This is a huge time-saver in larger projects.

Now we'll take a look at how an individual module is structured.

Pages: 1, 2

Next Pagearrow