An Ant Modular Build Environment for Enterprise Applications
Module Contents
We organize a module's directory structure corresponding to
common Java industry conventions for source code management.
Although there are different conventions, this is the directory
structure used in our build environment:
modulename
|-- build/
|-- etc/
|-- src/
|-- test/
|-- build.xml
Here's what each entry means:
-
build: This directory is special in that it is generated
by the module build. All other directories and files listed above
are entered into the RCS. The build directory contains all
files generated during the build process, from auto-generated XML
to compiled Java class files, and finally any distribution
artifacts (.war, .jar, .ear, etc.). This makes it very easy to clean
a build by just deleting this directory.
-
etc: This is a directory where all config files that are used by the module during build or run time are stored. Most
of the time, you'll find properties files and XML config files in
here, such as log4j.properties or struts-config.xml.
If there are a lot of files, they're typically organized into
subdirectories for the components they relate to; e.g.,
etc/spring/, etc/struts/, etc/ejb/, etc.
-
src: This directory is the root of your source file
directory tree. There are no other directories in it other than
those that directly correspond to a package and/or classpath
location. So you'll usually see a com/ or
net/ or org/ directory here starting a
com.whatever or net.something or
org.mydomain package structure. It is important to
note that only things that have a one-to-one classpath correspondence
are saved in this directory (i.e., package directories or .java
source files).
-
test: This directory is for your test classes (e.g.
JUnit test cases). The
important thing here from an organization perspective is the
package structure mirrors exactly that found under the
src directory. This makes it very convenient for
managing test cases, because you instantly know that the class:
moduleroot/test/com/domain/pkg/BusinessObjectTest
is a test case for the class
moduleroot/src/com/domain/pkg/BusinessObject.
This simple mirroring technique is very helpful in
managing large amounts of code. It's very easy to find your test
cases.
-
build.xml: This Ant file knows how to do everything
needed by this module to build and distribute the artifact for which it is
responsible. If this module has any submodules, it also knows
how to build those submodules and in which order they
should be built. Submodules and build ordering are very important
concepts that we'll cover shortly.
Submodules
A submodule is just a module that is a child of another (parent)
module. You might have seen other module-based Ant builds where the
hierarchy is flat; i.e., one level deep. Our build structure goes a
little further than that: ours is two levels deep.
Continuing with our build and the concept of submodules, you
would see a build hierarchy like the following, with the module and
submodule directories expanded:
module1/
submodule1.1/
|-- etc/
|-- src/
...
|-- build.xml
submodule1.2/
|-- etc/
|-- src/
...
|-- build.xml
build.xml
module2/
...
OK, so this looks a little complex. Why would we want to do
this?
Well, let's preface the answer with a little background on
enterprise applications and the concept of an
artifact-driven build.
Enterprise applications are almost always client/server-based.
Even if you only deploy a web application, it's usually architected
as a client-server MVC application. That is, the web page itself is
a client view, but the "server"-side components are usually
business POJOs that execute business logic on behalf of the
component rendering the web page. Even if they are deployed in a
single .war, there is a definite architectural separation
between code that is primarily used for rendering a view (client
code) versus code that is used for processing business requests
(server code). At least, there should be!
The notion of client and sever code becomes more obvious in a
more traditional client/server application where there is a
standalone client GUI communicating with a server-side business
object via sockets.
It would be very clean and elegant if we only needed to deploy
client code to the client application and server code to the
application server. Both tiers also probably share common code, so
it would be nice to send common .jars to both client and server.
This is the cleanest way to deploy code and manage dependencies
between tiers. Our build environment has the ability to create
artifacts exactly as desired.
Next we will look at how submodules help us achieve an
artifact-driven build.
Hierarchy and Build Artifacts
The deployment scenario just described surfaces a desire for an
artifact-driven build: each module or submodule in the build
environment should be responsible for creating an artifact that
will be deployed to the client or server or both. This is easily
done in our build environment by further breaking down the modules
in our sample application into common,
client, and server submodules. The
parent-child relationship and delegation of build responsibilities
is what makes this build hierarchical as well.
Using our sample application's admin module, lets
see what the hierarchy looks like in an expanded directory
tree:
appname/
|-- admin/
|-- common/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- client/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- server/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- build.xml
...
Each submodule's contents are structured as defined before, but there's a noticeable
difference.
The admin module does not have the typical module contents. It
just has submodules and a build.xml, and it doesn't
produce any artifacts itself. Instead it calls build targets in the
common/build.xml, server/build.xml, and
client/build.xml files via the template technique described earlier.
So if you wanted to build the admin module, you just change into
the admin directory and run Ant:
> cd admin/
> ant
This command uses the admin build.xml file, which in
turn builds the common, server, and
client submodules. After each submodule is built,
there will be three resulting artifacts:
appname-admin-common.jar
appname-admin-server.jar
appname-admin-client.jar
The common and server .jars can then be
deployed to the server (e.g., in an .ear file), and the
common and client .jars can be deployed to
the client (e.g., in a .war's WEB-INF/lib
directory).
What is the purpose of each submodule? Well, they help organize
code into cleanly managed subsets of functionality that will be
deployed in different tiers of the application. Here's what the
above three submodules typically contain:
common: All code that is common to both client
and server tiers for the module. This typically means business POJO
interfaces, utility classes, etc.
server: Class implementations only needed on the
server tier. These are generally implementations of business POJO
interfaces, DAO implementations for EIS access, etc.
client: Class implementations only needed on the
client tier, such as Swing GUI objects, EJB remote interfaces,
etc.
This kind of granularity of submodules and their respective
deployment artifacts benefits you in four substantial ways:
- Download times: You can ensure that standalone client
applications such as applets and Java Web Start
applications receive the smallest subset of .jars required to run.
This ensures the fastest possible download times of an application
or applet being run for the first time.
- Dependency management: Via an Ant
<path>
entry in the submodule's build.xml file, you can list
exactly which other module and/or submodules are allowed
as dependencies by the current submodule. This eliminates any lazy
or accidental use of APIs that a developer is not supposed to use
or won't be supported during runtime.
- Dependency ordering: Because the parent module determines
build order for submodules, you can rest assured that the
client code you write can depend upon
common code, but not server code. Also,
common code cannot be written that is dependent upon
server or client code. If you do these
things, your build will break, and you'll instantly be alerted that
you accidentally used classes that you shouldn't have. This may
sound like a small or nit-picky issue, but this problem quickly
rears its head in complex projects or those where the developers
have different levels of experience and may not be aware of
dependency management.
- Just as you can with modules, you can build just a single
submodule by entering in its directory and running
> ant
and Ant it will build only that submodule, saving you time.
Conclusion
Modules and submodules may look complicated. They probably look
like overkill to you at this point. But trust me from experience,
they greatly simplify how you manage source code and
dependencies, and how Ant builds your product. The structure
defined here really does make product-feature and source-code
management easier in a team environment. It takes a lot of the
guess work out of figuring out how to do all of the organization
yourself, and once set up, is pretty transparent. If you're
starting a new client/server project, give it a shot. You'll spend
more time working on your application, and less time worrying about
configuration management.
Special thanks to Jeremy Haile of Transdyn Controls for his valuable
input and review of this article.
Resources
- Sample code for this article
- Ant (see also O'Reilly CodeZoo: Ant)
- JUnit (see also O'Reilly CodeZoo: JUnit)
Les A. Hazlewood
is the director of software engineering at Roundbox Media in Atlanta, Georgia.
Return to ONJava.com.