Chapter 12. Optimal Builds

One of the first tasks you’ll undertake during the construction phase of a project is the design of the development environment. A well-designed environment is paramount to ensuring the entire project team can work productively and effectively.

In this chapter, we focus specifically on one aspect of the development environment that is critical to a successful rapid application development project, the build process. We cover in detail the importance of a fast, accurate, and maintainable build process, and examine the use of the Apache Ant build utility for producing optimal build environments for J2EE applications.

This chapter covers several areas regarding the build process:

  • The importance of an efficient build process for rapid development

  • The advantages of Ant as a build tool

  • How to manage build dependencies in order to reduce build times

  • The use of the open source tool Antgraph for graphically viewing dependencies in Ant files

  • Guidelines for organizing the code artifacts of J2EE projects

  • Suggested best practices for working with Ant

We conclude by revisiting the Jython scripting language and demonstrating how Jython scripts can readily extend the features of Ant.

Time and Motion

Anyone who has worked on a manufacturing production line is familiar with the concept of time and motion. A time-and-motion study involves the analysis of the operations needed to manufacture an item in a factory, with the intention of reducing the manufacturing time per item and thereby increasing the rate of output. The study calls for every step of the manufacturing process to be scrutinized in order to identify any inefficiency in the production method.

A production line has to be the epitome of efficiency, and production engineers use time-and-motion studies as a technique for streamlining the manufacturing process. Consider a car manufacturing plant, with vehicles rolling off lines in huge numbers. If one step of the process is suboptimal, the result could be a substantial increase in the cost of production as well as fewer cars being available for market.

Manufacturing time is not the only concern of the production engineer. Another factor is quality, as the process must ensure an acceptable and consistent level of build quality for every item rolling off the line.

Although a time-and-motion study is a technique used by the manufacturing industry, the efficiency-focused mindset of the production engineer is relevant to the task of designing a build process for software.

The Software Production Line

Few people would perceive the software development process as being similar to a production line. Developing software is a creative task and is different for each system. Nevertheless, the many and varied activities that make up a software development project are common to all projects and are frequently repeated by each member of the team on an almost daily basis.

A software engineer’s tasks include such common activities as pulling down the latest version of the application from source control; building, deploying, and testing new application functionality; and checking the defects log for errors raised against previous releases. These tasks and many more make up a typical developer’s day.

Similar recurring tasks exist for other project roles. The quality assurance team has a process for installing the latest releases from the development team and preparing a suitable environment for testing. The team may perform this task manually or rely upon a script for creating a clean environment for each test cycle.

For rapid development to take place, all of these activities must run smoothly and efficiently.

Time and Motion for Software Development

The software development environment and the processes and procedures that drive it must ensure all team members can work expediently and accurately. To achieve this optimal state for the project, teams should look to the practices of production engineers and perform their own time-and-motion studies on those everyday activities that are performed so frequently.

This shouldn’t be a matter of hiring production engineers with stopwatches to come in and time people as they work. Instead, efficiency is the responsibility of all team members, and everyone should seek ways to improve the process.

Feeding this information back into a company’s development practices is critical to maintaining a companywide adaptive foundation for rapid development, as passing these process improvements from project to project helps establish productive environments for future applications.

With the importance of time and motion in mind, let’s consider the intricacies of the build process.

The Build Process

A good build process is more than just a script for compiling software. Instead, a build process performs and automates many of the common activities on a project. Examples of common activities include the following:

  • Retrieving code from source control

  • Installing the correct versions of libraries and software

  • Compiling application libraries and components

  • Running automated test suites

  • Generating Javadocs

  • Packaging components and libraries

  • Setting up development and test environments, including updating database schemas and creating test data

  • Deploying applications into a development environment

  • Creating versioned releases of software components

  • Deploying releases into a test environment

Having a ready-made build process in place before a project commences is a major time-saver. Build processes are sophisticated pieces of software that require careful design, construction, and testing. Consequently, investment in the development of build processes that are reusable between projects is a key factor in the makeup of a company’s adaptive foundation for rapid development.

The next sections offer some guidelines for creating a build process that is conducive to the practice of rapid development.

Designing a Build Process

Like software systems, build systems require careful design. Regardless of the type of software under development, the requirements for a build system tend to be common between projects. Here is a summary of some of the main requirements:

  • Accurate.

  • The build process must be consistent between builds, producing the same result from a given set of source files for all developers on the team.

  • Rapid.

  • With builds running frequently, the process must be streamlined to ensure undue time is not lost waiting for the process to complete.

  • Automated.

    All steps of the build process must be under the control of a suitable build tool that ensures the entire build can be automated. If developers are required to undertake manual steps, such as copying files or building individual modules, then the possibility of introducing build errors into the process is increased. Furthermore, manual scripts cannot be set up to run as scheduled tasks that can operate overnight.

  • Standardized.

    How the build process is used should be consistent across projects and within teams.

  • Parameterized.

    A build for a developer is likely to be subtly different than a build destined for a formal test environment. These differences could be compiler options or the omission of build steps. The build process must be able to generate a consistent release for each type of environment.

  • Maintainable.

    Build environments have a tendency to increase in size and sophistication to the point that maintenance becomes a major headache, not to mention a black hole for lost time. The build system must be simple enough to be easily maintained yet capable of handling complex build tasks. Unfortunately, these two requirements are not complementary.

These requirements are common to most software projects. The development of enterprise-level software introduces a further set of requirements peculiar to the J2EE platform.

J2EE Build Requirements

The build process for a conventional Java-based application often builds only a single target, thereby making for a simple build environment. Unfortunately, a J2EE application is not so straightforward; it involves numerous intricate steps to generate multiple targets.

Unlike its J2SE counterpart, a J2EE application comprises a set of components, which collectively make up the whole application. Each component can have vastly different build requirements. EJB components require specialized compilation tasks for the generation of stub and skeleton implementations. Code generation tools such as XDoclet are becoming increasingly popular for the effort they save, but add additional steps and complexity to the build process.

Note

XDoclet is covered in Chapter 6.

In addition to these specialized tasks, J2EE components require packaging. EJB components are wrapped in JAR files, and Web applications are packed in a Web archive (WAR) file. Finally, all components can be bundled in an enterprise resource file (EAR), the format recommended for J2EE deployments by application servers.

To summarize, a J2EE build typically includes the following tasks, which are additional to traditional Java builds:

  • Execution of code generators

  • Component-specific compilation, such as for Enterprise JavaBeans

  • Packaging, for example, in JAR, WAR, and EAR files

  • Deployment

All of these tasks devour time. Packaging, which requires moving files around the file system into a structure where they can be bundled into a format ready for deployment, is a particularly time-consuming process. Likewise, the process of deploying components to the server and placing them in an executable state is also potentially time consuming.

Shortening the length of time spent building and deploying is a case of reducing the amount of work the build system has to undertake. This gives rise to the concept of a build system capable of performing minimal builds and minimal deployments. We consider each concept in turn.

Minimal Builds

Generating source, compiling code, and packaging binaries into JAR files are all resource-intensive tasks. If the number of times these tasks have to be performed as part of the build can be reduced, savings in build times result.

Achieving these savings involves ensuring the build system can undertake incremental tasks, building only those components impacted by a particular code change. Therefore, if a source file is changed, the build system should not have to regenerate the full system. Instead, it should be able to determine from dependency information those components affected by the change, and accordingly build, package, and deploy only those modules that are affected.

Minimal Deployments

Minimal builds are only part of the story. It is also necessary to consider how a built application transitions into a state in which it can be run and tested. For J2EE solutions, this involves deploying components to the application server.

A minimal deployment approach looks to reduce the total number of steps that must be taken by the software engineer in order to deploy a change to the server. A typical worst case is that the server must be stopped, the entire application redeployed, and then the server restarted. This type of delay is unacceptable. Thankfully, most J2EE application server vendors acknowledge the importance of being able to turn around change quickly and efficiently, so they offer support for the hot deployment of applications. The practice of hot deployment requires further explanation given its importance to the build process.

What Is Hot Deployment?

Hot deployment is a concept embraced by most application server vendors and refers to the ability of the server to deploy an updated J2EE application into a live environment without having to shut down either the server or the running application.

For a production system, hot deployment has obvious benefits for system availability. For development teams, however, we require a slightly different form of hot deployment that is perhaps better called automatic deployment.

This concept has the application server continually polling for new files. On detecting a new file, the application server immediately loads the change and integrates it into the current application.

warning

Automatic deployment is unsuitable for a production environment because the need for the server to poll its deployment directories continually is an unacceptable performance overhead.

In a fast-paced development environment, this functionality is a major timesaver for the software engineer, as no manual steps are needed for the application server to apply the software changes other than deploying the changes to the server.

Automatic deployment is a proprietary feature and is not covered by the J2EE specification, which only stipulates the deployment of EAR, WAR, JAR, and RAR files. Application servers, such as WebLogic from BEA, have gone beyond the J2EE specification and provide support for the deployment of applications in an expanded, or exploded, format rather than an archive file. This approach, which BEA recommends for deployment to its server, provides support for the minimal deployment approach.

WebLogic server supports automatic deployment if the server is running in development mode. This is achieved by setting the system property -Dweblogic.ProductionModeEnabled to false.

You must check the details of your particular application server to determine what it provides in terms of minimal and hot deployment capabilities.

Having covered some of the main requirements for a build system, we can now look at producing the build process itself. For this, we need to adopt a suitable build tool.

Introducing Ant

Ant is an extensible build utility from Apache Software Foundation that uses an XML-based syntax for creating build scripts. The build utility is open source and is available for download from the Apache Website at http://ant.apache.org.

Ant almost needs no introduction, as it has become the de facto standard for Java builds. As discussed in earlier chapters, XDoclet, Middlegen, and AndroMDA all rely on Ant for execution.

Note

Chapter 7 discusses Middlegen and Chapter 8 introduces AndroMDA.

Ant’s success is due to a number of features that see the build tool ideally suited to Java development. Ant’s use of XML documents for defining build files offers a clean and readily understandable syntax, making it possible for developers to get quickly up to speed with the tool. This was a significant problem with earlier Make tools whose declarative semantics were often difficult to grasp.

Furthermore, Ant is implemented in Java and runs under any compliant JVM. This makes Ant a crossplatform build utility, an important factor when working with the J2SE platform. This is also a big advantage over previous Make tools, which rely on platform-specific shell commands for performing build operations.

Due to its huge uptake, Ant has grown into a mature build tool that offers an extensive range of features for performing just about every conceivable build task. Under Ant, build files invoke build operations by calling Ant tasks.

Ant provides a set of core inbuilt tasks that perform many of the common build operations, including:

  • Compiling Java source

  • Defining build classpaths

  • Generating Javadocs

  • Copying and deleting files

  • Changing file permissions

  • Creating JAR files

  • Executing external applications

  • Invoking build steps in other Ant build files

  • Working with archive formats, such as ZIP and TAR

  • Sending mail

  • Accessing source control repositories

Where Ant does not support a specific build operation, developers can implement their own custom Ant tasks, which are then accessible from within the build file.

Most major Java software vendors supply Ant tasks for controlling the build process when using their software. The code generators of XDoclet, Middlegen, and AndroMDA all supply Ant tasks specifically for this purpose.

With Ant’s ubiquity in the Java world, you will likely use Ant for controlling all of your builds. Consequently, the next sections focus on the use of Ant for designing and implementing optimal build solutions.

Minimal Builds with Ant

For a build tool to support the concept of minimal, or incremental, builds, it must be able to identify the dependency relationships that exist between the various project artifacts that make up the application.

Prior to Ant, most Java developers used Make tools for creating build files. Make tools employ declarative programming language semantics for defining dependency rules between build artifacts. With this approach, the Make tool infers the build tasks to perform following a change to a particular source file or component.

Note

Declarative programming is described in Chapter 10.

The inference capabilities of the Make tool support a minimal build approach but at the expense of build-file complexity. Ant uses a simpler syntax for its build files than Make, but does not intrinsically support a declarative approach to the build process.

The Importance of Build Dependencies

To appreciate the impact of build dependencies, let’s consider the example build.xml in Listing 12-1. In this example, the build file instructs Ant as to the build order of each of the targets.

Example 12-1. Ant Example build.xml

<project name="ant-build" default="compile">

  <target name="compile"
          description="Compile all Java source">
    <javac srcDir="."/>
  </target>

  <target name="clean"
          description="Removes all class files">
    <delete>
      <fileset dir="." includes="*.class"/>
    </delete>
  </target>

  <target name="build"
          depends="clean, compile"
          description="Rebuilds all source"/>

</project>

The example build file in Listing 12-1 contains three build targets: compile, clean, and build. The compile target is set as the default for the project and is run whenever Ant is executed unless an alternative target, such as ant build, is explicitly specified.

Running the build file for the first time with the default compile target results in the compilation of all Java source in the base directory. For this example, assume we have a single HelloWorld.java file.

Running the build file a second time is distinctly quicker, as Ant determines all class files are up-to-date and no compilation is necessary. How is this possible given the build file provided no dependency information between HelloWorld.java and HelloWorld.class?

The secret lies in the <javac> task, used to compile the Java source. This task has built-in dependency rules and knows to associate *.java and *.class files. Therefore, for Java compilations, Ant supports the minimal build approach we require for fast builds. Unfortunately, Ant is not as knowledgeable about other file types. This is a problem, especially for J2EE builds that rely on many custom build steps.

Listing 12-2 revises the previous example to illustrate the problem.

Example 12-2. Build File with Dependent Target

<project name="ant-build" default="compile">

  <target name="generate"
          description="Long running build task">
    <ejbdoclet>
      .
      .
      .
    </ejbdoclet>
  </target>

  <target name="compile"
          depends="generate"
          description="Compile all Java source">
    <javac srcDir="."/>
  </target>

  <target name="clean"
          description="Removes all class files">
    <delete>
      <fileset dir="." includes="*.class"/>
    </delete>
  </target>

  <target name="build"
          depends="clean, compile"
          description="Rebuilds all source"/>

</project>

A new target, <generate>, has been added to the build. This target invokes a code generator, for example, XDoclet, which processes those files containing annotations.

Before the <compile> target can begin, the <generate> target must have completed. This dependency between the two targets is expressed using the special Ant attribute depends. With this dependency defined, Ant always runs the <generate> target ahead of <compile>.

The relationship specified between the two targets is procedural: Ant does not check file timestamps to determine if the <generate> target must be run.

Assume our example project comprised 10,000 source files, with 5,000 of them marked up with XDoclet-style annotations or attributes. Modifying an annotated file requires running XDoclet to pick up the change. However, if a file without annotations is changed, then we can safely skip the <generate> task and run only <compile>. Because the <compile> target uses the <javac> task, only the affected source file is compiled.

tip

Virus checkers slow down builds. Turning your virus checker off noticeably speeds up the build process. Unfortunately, the threat to unprotected machines from malicious viruses means companies tend to insist all machines run virus protection software at all times.

If you can’t turn off your virus checker, another option is to change the setting of the virus software so it ignores all files written to build directories. Most virus checkers are configurable to enable the exclusion of certain files and directories from the scanning process. Refer to the manual of your particular virus-protection software for more information.

Ideally, we want our code generator, be it XDoclet or some other tool, to have the same functionality as the <javac> task. If only one of our 5,000 annotated files is modified, then the code generator should process only a single file.

With this approach, the steps of the build process are as follows:

  1. Developer modifies a single annotate file.

  2. XDoclet generates new program source from only the file that has changed.

  3. <javac> task compiles only the files generated by XDoclet.

As it stands now, the <generate> target will pick up all 5,000 files, thereby resulting in the need for <javac> to undertake a significant recompilation effort.

This is a problem. Our build process does not support the concept of minimal builds, and our time-and-motion expert is far from pleased.

Using a Make tool, we could have defined build rules to instruct Make to run the code generator against modified files only. Unfortunately, Ant does not support this form of deterministic build process directly. More importantly, neither does the XDoclet Ant task.

Nevertheless, the authors of Ant recognized the importance of build dependencies and added support for this feature as a set of core tasks. We still have work to do if we are to make use of this functionality, as it is not default behavior. To understand how build dependencies can be enforced, let’s leave XDoclet and consider another example.

Defining Build Dependencies in Ant

The build.xml file shown in Listing 12-3 demonstrates the use of the <uptodate> task to define a conditional build dependency between two targets. In this example, the dependency defines the need to package Java binaries into a single JAR file.

Example 12-3. Ant Build File with Conditional Build

<project name="depend-example" default="make">

<!-- Build file with dependency defined between
     package and compile targets -->

<property name="src.dir" location="src"/>
<property name="bin.dir" location="bin"/>
<property name="dist.dir" location="dist"/>

<target name="compile"
        description="Compiles all Java source">
  <javac srcdir="${src.dir}"
          destdir="${bin.dir}"/>

  <!-- Check if files have been updated -->
  <uptodate property="package.notRequired"
            targetfile="${dist.dir}/app.jar">
    <srcfiles dir="${bin.dir}" includes="**/*.class"/>
  </uptodate>
</target>

<target name="package"
        depends="compile"
        unless="package.notRequired"
        description="Produces JAR file">

  <jar destfile="${dist.dir}/app.jar"
        basedir="${bin.dir}"/>
</target>

<target name="clean"
        description="Removes all class files">
  <delete>
    <fileset dir="${bin.dir}"/>
    <fileset dir="${dist.dir}"/>
  </delete>
</target>

<target name="make"
        depends="package"
        description="Incremental build"/>

<target name="build"
        depends="clean, make"
        description="Rebuilds all source"/>

</project>

The build process shown in Listing 12-3 involves two steps. First, the compile target compiles all code in the src directory with <javac>, directing all output to the bin directory. Second, the package target collects the contents of the bin directory into a single JAR file, placing the archive in the dist directory ready for deployment.

Two top-level build targets are responsible for performing these steps:

  1. The build target deletes all built files and compiles and packages the application from scratch.

  2. The make target performs an incremental build and creates a distribution only if any source has been updated.

Packaging large numbers of files into JAR files is a time-consuming process, so it is of benefit to perform this task only when needed. We make the running of the package target conditional with the use of the unless attribute.

The unless attribute instructs Ant to skip the execution of the target if the associated property has been set to any value. Conversely, the if attribute of <target> instructs Ant to run the target if the property is set.

With properties and the if and unless attributes of the <target> task, we can control at build time which targets are run. From the example in Listing 12-3, the package target references the package.notRequired property to determine if it should generate a JAR file. This property is set as the final act of the compile target upon which the package target is dependent.

The task <uptodate> is used to set the package.notRequired property in the example. This conditional task sets the property identified with the property attribute if the target file is more up-to-date than the source. In this case, the <uptodate> task checks to see if any class files have been generated since the JAR file was last produced. If any class files prove to be more recent than the JAR file, then the package.notRequired property is left unset and the package target is allowed to execute.

The conditional support provided by Ant makes it is possible to create sophisticated build scripts that support incremental builds. However, adding conditional behavior to the build process can significantly increase its complexity. The example in Listing 12-3 covers a very basic case. A typical J2EE build involves many more targets, and adding dependency information therefore adds significant complexity to the build process.

To prevent additional build logic making build files unduly complex, good design dictates the build process be broken down into discrete blocks, or modules. With this approach, modules of the system can be built in isolation, and dependencies can be more easily defined between build targets.

Working with Subprojects

Good software design sees a large application broken down into smaller modules that exhibit the characteristics of loose coupling and high internal cohesion. A build script is itself a software artifact, so the same rules apply when designing a build process, and we should look to break large builds down into smaller, self-contained units known as subprojects.

Ant provides a number of options for breaking down large build files into smaller, more manageable modules. One approach is to have a central, controlling build file responsible for delegating build tasks to each of the subprojects. Ant allows targets to be invoked between build files using the <ant> task.

The following extract depicts the use of the <ant> task to kick off a target in a separate build.xml file.

<ant antfile="build.xml"
     dir="${subproject.dir}"
     target="package"
     inheritAll="no">
  <property name="package.dir"
          value="${wls.url}"/>
</ant>

Attributes of the <ant> task specify the name of the build file, its location, the name of the target to be invoked, and whether the subproject is to have access to all the properties of the caller. The default for this final attribute is to have the called build file take on all the properties of the caller. In the example shown, this behavior is disabled, and instead, specific properties are passed in using the nested <property> element.

Breaking up build files in this manner is not to everyone’s liking. Some people prefer keeping all the build processing in one location. Ant can support this preference by allowing build files to pull in other build files rather than delegate out to them.

Ant can achieve the include-type functionality of Make in one of two ways. Ant files are first and foremost XML documents, so we can leverage the power of XML to pull in build snippets. Listing 12-4 demonstrates this approach.

Example 12-4. Including Build File Snippets

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE project [
  <!ENTITY properties SYSTEM "file:./config/properties.xml">
  <!ENTITY libraries SYSTEM "file:./config/libraries.xml">
]>

<project name="include-example" default="make" basedir=".">

  &properties;
  &libraries;

  .
  .
  .

</project>

From the example shown in Listing 12-4, the contents of the two files properties.xml and libraries.xml are inserted directly in the build.xml file at the location marked with &properties and &libraries respectively.

Ant 1.6 provides a second method for pulling in external build files. The <import> task, which pulls in an entire build file, has the advantage over the previous example in that it enables build files to inherit from other build files. Thus, we can use this behavior to override targets in imported build files if required.

Viewing Build Dependencies

Dependencies within build files can become very complex and involved. Thankfully, Eric Burke has come up with Antgraph, a very convenient open source utility for graphically viewing Ant build files. Antgraph is available from Eric’s Web site at http://www.ericburke.com/downloads/antgraph/. The software is very easy to install and run, and is available for several platforms.

Figure 12-1 shows an example of the output from Antgraph.

Ant build file dependency information produced by Antgraph.

Figure 12-1. Ant build file dependency information produced by Antgraph.

The example in Figure 12-1 is a graphical representation of the Ant build file supplied by AndroMDA for producing a Hibernate-based AndroMDA project. The graph depicts each target within the build file, with the arrows showing the calling hierarchy between the targets. The numbers on the arrows are a count of the dependencies for each target. For example, build is dependent on three targets: init, buildjars, and hibernate-schema.

Antgraph relies upon Graphviz, a freely available graphics engine, for rendering the graph. Graphviz supports a number of different graph types, so you can experiment with the different options. The graph shown uses the standard dot layout engine, but other options include the neato and twopi layout engines. You might want to experiment to find your favorite.

Standard Build Targets

It’s a good idea to try to standardize on the targets across build files. Adopting a target-naming convention ensures all build files between projects are consistent and more easily understood and maintained by members of the project team. Table 12-1 offers some naming suggestions for the common Ant build targets.

Table 12-1. Naming Suggestions for Ant Build Targets

Name

Reference

init

General utility target for performing any setup tasks, for example, creating directories, copying files, or performing dependency checks. This target is seldom invoked directly from the command prompt but is instead called by other Ant targets.

clean

The clean target removes all generated build artifacts, thereby ensuring any subsequent builds regenerate all targets.

compile

This target compiles all source files. It may also be dependent on a generate target, which invokes any code generators that are part of the build process.

package

The package target takes the output from the compile phase and packages up all files ready for deployment. This target is sometimes named dist.

make

Use the make target for initiating incremental builds. This target requires conditional dependencies to be correctly set up between the different build targets.

build

Performs a clean followed by a make, thereby ensuring the entire application is built from scratch.

deploy

Deploys the built application to the designated application server. An undeploy task is also useful for removing the application from the server.

test

Use this target for running all unit tests. The test target should be run regularly and preferably included as part of an overnight build process.

docs

Generates all the Javadoc documentation for the project.

fetch

Retrieves the latest version of the project from source control. You might want to consider defining an overarching target that fetches the latest code, performs a complete build, and then runs all unit tests.

These suggestions are for a minimal set of build targets; your project will probably have many more. For example, you could add a run target to launch any client applications as well as start and stop targets for controlling the application server.

Applying a consistent naming convention becomes more important with Ant 1.6, as the use of <import> makes it possible to override the targets of other build files within an inheriting Ant build file.

tip

Ensure you provide a good level of help for all targets within the build file. Use the description attribute of the target element for this purpose. You can then launch Ant with the –projecthelp option to see a list of all targets, complete with descriptions, that are supported by the build file.

You must specify dependencies between each target. Figure 12-2 shows the output from Antgraph based on the targets defined in Table 12-1.

Target hierarchy as generated by Antgraph.

Figure 12-2. Target hierarchy as generated by Antgraph.

For larger projects comprising multiple modules, we need to consider how the overall project is organized. The next section provides guidelines for addressing this issue.

Project Organization

A well-organized project directory structure makes both the code and the build process easier to manage. A number of factors, including the requirements of the target application server and the needs of the development tools used by the project team, drive the exact structure of your particular build process. However, the concepts presented here provide an outline that you can tailor for your specific project environment.

note

The project structure becomes more elaborate as the application grows in size. By keeping things simple from the start, you can help avoid undue complexity as the system evolves.

For J2EE development, the project structure falls logically into the four separate areas: source, build, external libraries, and distribution. Each area is normally contained within its own directory on the file system, with each directory sitting directly beneath a top-level project directory in the hierarchy.

Figure 12-3 depicts the top-level project structure.

Top-level directory structure.

Figure 12-3. Top-level directory structure.

We start by examining the structure of the source directory.

Source Directory

The source directory contains all program artifacts involved in the construction of the application. This is not limited to Java source but encompasses such artifacts as build scripts, property files, deployment descriptors, database scripts, and XML schemas. Figure 12-4 shows the organization of the source directory.

Source directory structure.

Figure 12-4. Source directory structure.

The project is structured around the creation of a single EAR file, with each module of the distribution being maintained as a subproject.

note

All source code should reside under its package directory structure. You should also consider adding a parallel source directory for each component to maintain the code for unit tests.

From the directory structure shown in Figure 12-4, we have several modules:

  • The META-INF directory holds all the files necessary to make up a deployable EAR file.

  • EJB modules are placed in their own directory. An EAR file can be comprised of several modules. The example shown in Figure 12-4 contains a single EJB module, MyEJB.

  • Common libraries built as part of the project should reside within the directory structure, as is shown with the library MyLib.

  • Web applications have the most complex structure because they are made up of several file types. The Web application MyWebApp shows a possible layout for the organization of the different file types.

Each module that makes up the J2EE distribution possesses its own build.xml file. These are self-contained build files, capable of building each module in isolation. The build files are also aware of dependencies that may exist between the different modules. A Web application will likely call into a business layer and so may have a dependency on the EJB module. This should all be managed by each build file.

All build files should be consistent between modules, with each build file exposing the same set of targets. A single controlling build file resides at the top of the project directory structure. This is a delegating Ant build file, again exposing the same targets as the build files of each subproject.

Calls to targets in the project build file are cascaded down to the equivalent targets in each of the subprojects. Although each build file has the same set of targets, the work they perform is in context with the type of module they are responsible for building. On a Web module, the target package produces a WAR file, while on the top-level project build file, the package target generates an EAR file.

The build order of the subprojects is important and should commence with the low-level modules and work upwards. Building from bottom-to-top and ensuring hierarchical dependencies are in place provides a build system that is easy to maintain and consequently produces consistent results.

Lib Directory

This directory holds all external libraries. The build scripts reference this directory structure during compilation. When assembling a distribution, libraries required at runtime are copied from this directory as needed.

Build Directory

All output from the source directory is directed to the build directory. Keeping source files separate from built files is important for good housekeeping. A clean separation of the two directories means the build directory is disposable and can simply be deleted at any time. This makes the writing of the clean target a trivial task.

The structure of the build directory should mimic that of the source for consistent navigation of all built files. It is considered good practice to direct all output from the build process to the build directory, including output from code generators such as deployment descriptors and remote interfaces. The simple rule to follow is to place all output that will be created each time a full build is run under the build directory, regardless of whether the output is source or binary.

Figure 12-5 illustrates the structure of the build directory based on the project structure defined for the source in Figure 12-4.

The build directory structure.

Figure 12-5. The build directory structure.

Having built all the necessary parts of the project, the next step is to package all components ready for deployment to the application server. For this task, we make use of the distribution directory.

Distribution Directory

The distribution directory is convenient for packaging and deployment purposes. It is possible to undertake all of this work within the build directory, but having a separate directory for all packaged files simplifies the process of generating formal releases or passing new versions of the system onto a formal test environment.

Figure 12-6 depicts an example of a distribution directory. Again this directory structure adopts the module structure seen in both the source and build directories.

The distribution directory.

Figure 12-6. The distribution directory.

Having the distribution directory fall under the main project directory is optional. For development purposes, you may wish to generate the packaged files directly into the directory structure of the application server to take advantage of its hot deployment capabilities. In some cases, the application server may call for all modules to be deployed in an uncompressed format, bypassing the need for extensive packaging.

The project structure outlined is a guideline only. The requirements of your project dictate the workings of your build process, and you should design a project structure according to your specific needs.

Integration with IDEs

Although it is important to be able to build the entire application from the command line, the use of a build script should not preclude the use of the productivity features of your favorite integrated development environment (IDE). While is important that an IDE is able to interoperate with a build script, it is equally important that a build script can utilize the features of the IDE.

Note

Chapter 13 covers the advantages of working with an IDE.

Unfortunately, IDEs do not make good build tools. They tend to do only a very small subset of the build tasks required on most projects. They can perform such tasks as compilation and packaging, but fall short of supporting all the build tasks necessary for the development of enterprise software. In contrast, a good build script performs tasks such as stamping JAR files with version numbers, packaging applications ready for deployment, distributing applications into test environments, running unit tests, and so on.

Although IDEs do not perform all the essential build tasks required for a project, they do perform certain build tasks extremely well. Compilation of all source files is one example. A highly useful feature of an IDE is to have it continually compile and syntax-check code as you type. This feature traps syntax errors early on and is very productive.

To support this feature, the IDE must be configured with all the necessary information to compile the application. If the build script is also to perform the compile task, a duplication of effort exists between the IDE and build script. Such a duplication, as well as being an overhead to maintain, is a possible source of build inconsistencies, since different application characteristics may emerge from the different build approaches.

One way to circumvent this problem is to have Ant use the IDE to perform the build. With this method, builds initiated from the IDE are identical to those invoked from the build script.

In order for this approach to work, the IDE must be able to run a build from a shell command, preferably without launching the entire graphical development environment.

Here’s how the Ant <exec> task can build the project using Borland’s JBuilder.

<exec dir="."
      executable="${jbuilder_home.dir}/jbuilder.exe"
      failonerror="true">
  <arg line="-build myproject.jpx make"/>
</exec>

This approach relies on using the machinery of the JBuilder IDE, complete with the JBuilder project’s classpath settings, to perform the build. This removes the need to duplicate build information between the project file of the IDE and the Ant build file. Check the manual of your favorite IDE to establish how to initiate the build process from the command line.

Extending Ant with Jython

Though Ant comes complete with an extensive range of built-in tasks suitable for most build-related operations, you will likely encounter situations in the design of your environment that are not accommodated by the existing Ant tasks. In this scenario, we can use Ant’s extensibility to define our own tasks.

Ant tasks are traditionally implemented in Java. However, as build tasks are usually the domain of scripting languages, another option is to use Jython. The advantage of this approach is that we get the rapid development benefits of the Jython scripting languages as well as access to Jython’s extensive library of functions.

Note

Chapter 9 covers the basics of the Jython language.

Creating a New Ant Task

Implementing a basic Ant task involves the following steps:

  1. Define a Java class that extends org.apache.tools.ant.Task.

  2. Provide public setter methods for all task attributes.

  3. Implement a public execute() method.

Listing 12-5 shows an implementation of an Ant task in Jython. The task has a single attribute called message. Invoking the task from an Ant build file prints out the message held in the attribute and then prints further information on the owning target’s dependency information.

You can use this example as a template for creating your own Jython Ant tasks.

Example 12-5. Jython Ant Task RapidTask.py

from org.apache.tools.ant import Task
from org.apache.tools.ant import Target

class RapidTask(Task):

  # Overrides the Task.execute() method
  #
  def execute(self):
    "@sig public void execute()"

    # Print out properties of task
    #
  self.log('Task Name: ' + self.taskName)
  self.log('Description: ' + self.description)

  # Print out the message property
  #
  self.log('Message: ' + self.message)

  # Get the owning target and list dependencies
  #
  target = self.owningTarget

  self.log('Target name: ' + target.name)

  for dependency in target.dependencies:
    self.log('	Depends: ' + dependency)

# Ant attribute setter method
#
def setMessage(self, message):
  "@sig public void setMessage(java.lang.String str)"
  self.message = message

Compiling Jython Classes

The Jython class shown in Listing 12-5 has a few differences from a normal Jython class, as it must be complied if Ant is to use it. Compiling Jython to standard Java bytecode involves moving from the type-less world of Jython to the strongly typed world of Java. This presents a few problems when defining methods on Java classes as Jython methods have no signature.

To get around this problem, it is necessary to embed a string in the __doc__ namespace of each method that defines the Jython method’s Java signature. Here are the embedded method signatures for both the setMessage() and execute() methods on the RapidTask class:

@sig public void setMessage(java.lang.String str)
@sig public void execute()

The @sig preamble at the start of the string tells the jythonc compiler the Java method signature to generate. This is all jythonc needs to compile the class to bytecode.

The Jython class is compiled from the command line:

jythonc -a -c -d -j rapidTask.jar RapidTask.py

The parameters -a, -c, and, -d instruct jythonc to include the entire core Jython libraries in the build. The –j rapidTask.jar specifies to place the compiled classes into the named JAR file.

With the new task successfully compiled, it is ready for use from within a build file.

Testing the New Task

The new task is included in the build script using the <taskdef>, which defines the name of the new task and the class to load. Listing 12-6 shows an example of an extract from a small test build file for the new Ant task.

Example 12-6. Test Build File for the Jython Ant Task

<target name="test"
      depends="clean, package"
      description="Access the Jython Ant task">

  <!-- Declare the new Jython task -->
  <taskdef name="Rapid"
            classname="RapidTask">
    <classpath>
      <pathelement location="${task.jar}"/>
    </classpath>
  </taskdef>

  <!-- Set a property on the task -->
  <Rapid description="Example task"
          message="My Jython Task" />

</target>

Invoking the <test> target generates the following output from the Jython Ant task.

test:
    [Rapid] Task Name: Rapid
    [Rapid] Description: Example task
    [Rapid] Message: My Jython Task
    [Rapid] Target name: test
    [Rapid]     Depends: clean
    [Rapid]     Depends: package

BUILD SUCCESSFUL

The Jython task uses methods on the Task class to obtain information about its environment, such as the name of the task, the name of the owning target, and a list of all dependencies. It also prints out the message attribute we added as part of the task.

That’s all there is to building an Ant task with Jython. Mixing Ant and Jython gives us the structure and control of Ant coupled with the rapid scripting capabilities of Jython—the perfect combination for constructing build processes.

Summary

The build process forms only part of the overall software development process. Nevertheless, the frequency with which the process is invoked is such that small inefficiencies result in substantial time penalties.

Designing a generic build process that is usable by all project teams is an important building block in the creation of an adaptive foundation for rapid development. Having an optimal and accurate build process in place at the start of the project provides a significant time saving.

The effort spent on defining a common build process is a good investment for the long-term success of all projects, so don’t skip this vital development task. Remember to treat the build process as a time-and-motion study and remove any unnecessary steps.

With the build process covered, the next chapter looks at the development tools that should be a part of any software engineer’s toolbox.

Additional Information

Ant is a tool for building software. The folks at Apache chose Ant as the basis for Maven, a tool for performing project management, build, and deployment tasks. Maven provides a formal framework for developing software artifacts and strictly enforces dependencies between build targets.

For more information on Maven, and to download the latest version of the software, see http://maven.apache.org.

An important part of the software development environment not covered in this chapter is the mechanism for the integration of software produced by team members. Bringing together newly developed pieces of functionality can often result in a broken build process, a major source of wasted time on any project.

A solution to the software integration problem is to adopt a policy of continuous integration. This idea involves integrating software changes frequently, possibly several times a day, and is the brainchild of Martin Fowler and Matthew Foemmel. The pair wrote their ideas up in a paper that can be viewed at http://www.martinfowler.com/articles/continuousIntegration.html.

The concept of continuous integration is backed by the open source product CruiseControl, a framework that integrates with your build system to perform regular builds and reports on build status. The CruiseControl software is obtainable from http://cruisecontrol.sourceforge.net.

Finally, for anyone who wishes to experiment with make files, a copy of GNU Make is available from http://www.gnu.org/software/make.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.76.227