Develop an environment-aware Maven build process

Extend Maven builds to port Java apps to multiple environments

Traditional Java applications capture runtime variables in property files, then package them into individual JARs for various software environments. But for users of Maven, it could make better sense to include environment variables in the build process. In this article, Paul Spinelli shows you how to extend the standard Maven build in order to port any Java application to multiple environments.

A Java application hosted within an application server usually requires access to a set of key-value pairs that provide environment-specific runtime variables, such as a JDBC connection URL. In this article, I demonstrate a different approach to developing environment-aware applications, by making environment variables part of the Maven build process.

Developing an environment-aware build process involves introducing two Maven plugins and two small projects to your code. Specifying the build environment as a JVM argument during the Maven build phase will dynamically set property files to reflect the correct values for the given environment. All of the property files will be placed in a directory accessible to the classpath instead of being bundled into JARs.

An environment-aware Maven build process can be customized to support any software environment. The potential benefits of such a build process extend to developers, configuration managers, and administrators involved in building and deploying application artifacts.

About environment variables

A runtime variable is any variable that may change in the environment to which the software is deployed. For example, most Java applications utilize a relational database to persist state, and the database's JDBC connection URL and associated user credentials will differ between environments. The JDBC connection URL and credentials are runtime variables.

As a software developer, you cannot completely avoid runtime-variable dependencies. But you do have options as to how your software will obtain the information required to interact with those dependent systems in different environments. A common approach is to use the operating system hosting the software in order to discover its dependencies. In Windows, for example, you could use the Registry to capture environment-specific runtime variables upon the installation of your application. Another option would be to encapsulate environment variables as key-value pairs in property files bundled with your application. These property files would then be made accessible to developers and administrators.

In addition to the runtime environment variables, applications usually also externalize certain software configurations that are not required for the software to execute (such as the default values included with a typical install). These software configurations allow users and the installer some control over the software's runtime behavior. Often these are stored in a database or, again, in property files.

When deciding whether to include property files as part of a software project, you must consider not only how the properties will be accessed and modified during the install and runtime phases of the application lifecycle, but also during the development and test phases. These decisions can have a significant impact on the maintainability of your product.

Environment-aware dependency management in Maven

You may be familiar with using Maven build profiles to load variables into the build process for different environments. Variables defined within a build profile are available within each pom.xml intended for use in building deployable artifacts. Build profiles are powerful, but using them requires manually deploying a settings.xml file to the build machine's $USERHOME\.m2 directory or $M2_HOME/conf/settings.xml directory. This makes the resulting application less portable than we would like. Build profiles also do not easily handle deploying different sets of property files on a per-application and per-environment basis.

In this article I'll demonstrate an alternative to using Maven build profiles to develop an environment-aware build. Working in an environment-aware build environment means that when software is compiled and/or built into a deployable package, all of the property files will exist as part of the project and will be properly configured for the target environment.

While this might not be so important to developers focused on working within a development environment, it does become important during other phases of the software development life cycle (SDLC). For example, it's normal during a software maintenance cycle for the configuration-management team to re-package software for a test environment, prior to any release to a production environment. A test environment usually mirrors the production environment, so it must be connected to all of the external systems required to properly host and test all functions provided by the software. Configuring the software's property files for use during deployment, even under test, should be a smooth and easy process, and it should not require the assistance of the development team.

For projects already using Maven, it makes sense to enhance the existing Maven build system rather than introducing a different system or set of dependency management tools.

Project setup

In order to follow the examples, you'll need the following setup in your development environment:

  1. App-Config: A Maven project that contains all of the property files as templates for three environments: development, testing, and production. App-Config also contains a master property file for each environment, which contains property key-value pairs for all application variables used at runtime.
  2. Parent-Pom: A Maven project hosting a parent POM for inheritance by other Maven projects.
  3. The maven-dependency-plugin and maven-resources-plugin. These will be used by the Parent-Pom project's POM and by individual project POMs to copy and filter property-file templates.

Demonstration setup and source code

The Maven projects used for this article build a Mule ESB 3.2 application using the Eclipse m2eclipse Maven plugin and the Eclipse Maven Mule plugin. The Maven projects were built within Eclipse Indigo. While you do not need to download the projects to follow the examples, doing so could be helpful. See Resources for more about the development environment setup.

The example application: Mule ESB

A Mule ESB application consists of a ZIP file that contains classes and JARs, resources, and a mule-config.xml file. These contents are placed in specific areas of the ZIP file, much like a WAR file dictates a specific directory structure. The Maven Mule plugin constructs the ZIP artifact during the package phase, as well as copying the files into the correct directory locations within the ZIP. The Mule plugin also includes dependent JAR files within the ZIP.

To demonstrate the environment-aware build process, we'll construct a Mule ESB application that contains two other applications (bundled as JARs), each containing its own property files. One of the JARs will contain JDBC MS SQL Server database connection code, which reads the database connection information from a property file called database.properties. The other JAR will contain a SOAP Web Service client, which uses a property file to load the information required to connect to a server hosting the Web services.

It should be noted that the choice of using the Mule ESB was made only because its resulting Maven build artifact is a ZIP and the ZIP folder structure lends itself cleanly to this example. A project that builds a WAR artifact would have been just as valid. Also note that usually, you'd let your ESB handle establishing and managing database and Web service connections but again, the Mule ESB was chosen for the artifact it generates.

The entire solution as applied to this example consists of three applications, each configured as a Maven project:

  1. The Mule ESB Maven project
  2. The Database Maven project
  3. The SOAP Web Service Client Maven project

For the purposes of this article, it's not that important why or how the Mule ESB app uses the other two projects, so I won't spend time on the details of their functionality. Instead, we'll focus on properly configuring the property files for all three projects. When the Mule ESB project is built into its ZIP artifact, all of the property files bundled within the ZIP must contain properties specific to the given deployment environment. Since the Mule app's two JARs connect to external systems and use property files to obtain information about these external systems, we can assume that this information may change per environment.

Figure 1 is an overview of the extended Maven build system.

Figure 1. A diagram of the proposed Maven build solution (click to enlarge)

Unpacking the example applications

Let's take a look at the pom files for each of our applications, starting with the Mule app in Listing 1.

Listing 1. Mule ESB application pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.techflow.bcs3.ldp.mule-esb</groupId>
    <artifactId>mule-app</artifactId>
    <packaging>mule</packaging>
    <name>Mule Application</name>
    <description>Mule application that connects to both a database and Web service.</description>
    <version>${app.version}</version>
    
    <properties>
        <app.version>1.0-SNAPSHOT</app.version>
        <mule.version>3.2.0</mule.version>
        <database.version>1.0-SNAPSHOT</database.version>
        <web-service-client.version>1.0-SNAPSHOT</web-service-client.version>       
    </properties>
    
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.3.1</version>
                </plugin>
                <!--This plugin's configuration is used to store Eclipse m2e settings 
                    only. It has no influence on the Maven build itself. -->
                <plugin>
                    <groupId>org.eclipse.m2e</groupId>
                    <artifactId>lifecycle-mapping</artifactId>
                    <version>1.0.0</version>
                    <configuration>
                    <!-- Content not displayed in this article for brevity purposes. -->
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.mule.tools</groupId>
                <artifactId>maven-mule-plugin</artifactId>
                <version>1.7</version>
                <extensions>true</extensions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
            </plugin>
        </plugins>
    </build>
    !-- Mule Dependencies -->
    <dependencies>
        <dependency>
            <groupId>xalan</groupId>
            <artifactId>xalan</artifactId>
            <version>2.7.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>xerces</groupId>
            <artifactId>xercesImpl</artifactId>
            <version>2.9.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>xml-apis</groupId>
            <artifactId>xml-apis</artifactId>
            <version>1.3.04</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.foo</groupId>
            <artifactId>database</artifactId>
            <version>${database.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.foo</groupId>
            <artifactId>web-service-client</artifactId>
            <version>${web-service-client.version}</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

Figure 2 shows the Mule ESB application's Maven project directory structure as viewed in Eclipse Indigo.

Figure 2. Mule ESB directory structure
1 2 3 4 5 Page 1