One artifact with multiple configurations in Maven

Problem

When working on www.beertoplist.com I ran into a Maven problem, that is fairly common: Having a project that should be configured differently for different environments. That is for instance you want one configuration for development, one for test and one for production. I wanted a solution that allowed me to make changes to all kind of configuration files, for instance property files, Spring context files and Tomcat context files.

Poor solution

Googling on the problem I found that most people suggested that you should use a set of Maven profiles, one for each environment. So when you want to build the project for test environment you enable the test profile and get all the test configuration. At first glance this might seem like a good solution, but there is a major problem with this solution.

When you have your artifact, how do tell which environment it is configured for? Even worse what if you forget to enable the production profile when releasing or maybe enable the test profile instead. Then you have released a test version of the project and there is no way you can tell except for checking the details in the artifact. Further more you commonly want to deploy the released version to a test environment, but you can not do that out of the box, because the released artifact in the Maven repository is configured for production (if you enabled the right profile). Thus you need to check out the release tag and build a new version with the test profile enabled. Now you end up with two files with the same name and version, that are configured differently. Not a very appealing solution.

My solution

My suggestion is to instead let Maven build one artifact for each configuration and label them accordingly, so when looking at a specific version in the Maven repository you find, one development artifact, one test artifact and one production artifact, each clearly labeled. Then you never run into the problems listed above. So how can we accomplish this?

Maven have the concept of classifiers. This is a suffix that is appended to the name of the artifact to distinguish it from the main artifact. This is used for instance by the source plugin that creates a source jar file for a Maven project, resulting in a file with the name <artifact id>-<version>-sources.jar. The classifier for this file is “sources”. The same concept can be used to build a development, a test and a production artifact, which is the solution I choose in www.beertoplist.com.

I decided to put all the environment specific configuration in a special source tree, with the following structure:

+-src/
  +-env/
    +-dev/
    +-test/
    +-prod/

Then I configured the maven-war-plugin to have three different executions (the default plus two extra), one for each environment, producing three different war files: beer-1.0-dev.war, beer-1.0-test.war and beer-1.0-prod.war. Each of these configurations used the standard output files from the project and then copied the content from the corresponding src/env/ directory on to the output files, enabling an override file to be placed in the corresponding src/env/ directory. It also supported copying a full tree structure into the output directory. Thus if you for instance wanted to replace the web.xml in test you simply created the following directory:

src/env/test/WEB-INF/

and placed your test specific web.xml in this directory and if you wanted to override a db.property file placed in the classpath root directory for the test environment you created the following directory:

src/env/test/WEB-INF/classes

and placed your test specific db.property file in this directory.

I kept the src/main directory configured for development environment. The reason for this was to be able to use the maven-jetty-plugin without any extra configuration.

Configuration

Below you find the maven-war-plugin configuration that I used to accomplish this:

<plugin>
  <artifactId>maven-war-plugin</artifactId>
  <configuration>
    <classifier>prod</classifier>
    <webappDirectory>${project.build.directory}/${project.build.finalName}-prod</webappDirectory>
    <webResources>
      <resource>
        <directory>src/env/prod</directory>
      </resource>
    </webResources>
  </configuration>
  <executions>
    <execution>
      <id>package-test</id>
      <phase>package</phase>
      <configuration>
        <classifier>test</classifier>
        <webappDirectory>${project.build.directory}/${project.build.finalName}-test</webappDirectory>
        <webResources>
          <resource>
            <directory>src/env/test</directory>
          </resource>
        </webResources>
      </configuration>
      <goals>
        <goal>war</goal>
      </goals>
    </execution>
    <execution>
      <id>package-dev</id>
      <phase>package</phase>
      <configuration>
        <classifier>dev</classifier>
        <webappDirectory>${project.build.directory}/${project.build.finalName}-dev</webappDirectory>
        <webResources>
          <resource>
            <directory>src/env/dev</directory>
          </resource>
        </webResources>
      </configuration>
      <goals>
        <goal>war</goal>
      </goals>
    </execution>
  </executions>
</plugin>

This Post Has 15 Comments

  1. Anders Hammar

    Well written! Having different resulting artifact from one build depending on active profile(s) is not the Maven way, for the reasons you pointed out.
    However, what I always try is to keep configuration stuff out of the archive. Those should be loaded from the class path instead. By doing this you’ll be able to use exactly the same archive (ear for instance) in all environments. From a test perspective this is very attractive. Going production with something that you’ve never really tested (when you have different archives) is less attractive IMHO.

  2. Tor Arne Kvaløy

    Hey, I just want to write and say thanks for this great article. It really helped me solve a major problem in my current project!

    Thanks.

  3. Graham

    works a treat, thanks!

  4. Tobi

    Hey Hendrik,

    really great. This one saved me a lot of trouble.

    Thank you,

    Greetz
    Tobi

  5. Hayden Steep

    I… I think I love you.
    Great article. Thank you so much!

    I had a monster of a problem where I have a WAR application that, regardless of test/prod/dev, can be built using 2 different configurations that cause the application to behave differently. BOTH of these configurations run in parralell on the samer server. They are packaged inside of an EAR and I had no idea how to get both versions in there. (Sure I can change the build with profiles, but then 1 artifact will overwrite the other, and I need 2 artifacts) Your explanation of the classifiers is one of the easiest to understand. Thanks!

    1. Yuko

      The only caveat is that you DO NOT want to do this for crtcaiil security scripts because it opens you up to path based exploits. I would extend this to say The only caveat is that you DO NOT want to do this for any script that root or any user that is in a crtcaiil group might ever run. And what does that leave, exactly?Things going wrong with differing paths to bash/perl/ruby/etc. are a problem, but things going wrong with path based exploits are several orders of magnitude worse.

  6. Suresh Bhaskaran

    Thank you, Henrik! This is an excellent post. I simplified your script to make it build one war at a time, by passing env variable, like:
    (1) mvn clean package
    (2) mvn clean package -Denv=test
    (3) mvn clean package -Denv=prod

    The first case will default to dev (by passing no arguments). The second is for building test, and the last one for prod.

    You still define all the 3 directory structures. But the pom is very simplified now. I removed the executions altogether, and removed the classifier as well (so that the war file name does not have -test or -prod at the end. If you want all the 3 wars be created at the same time, stick with the original pom as shown by Henrik) Here is the relvent modifications:

    org.apache.maven.plugins
    maven-war-plugin
    2.1

    false
    ${project.build.directory}/${project.build.finalName}

    src/env/${env}

    Also, some where in the top of the pom, add the following:

    test

    Please let me know if someone needs the full pom. Again the original credit goes to Henrik!
    Hope that helps.

  7. Suresh Bhaskaran

    Actually the pom portion did not appear correctly in the post. Reposting the pom changes without the opening and closing brackets: (email me at sureshbhask gmail.com if you need the full pom).

    %plugin%
    %groupId%org.apache.maven.plugins%/groupId%
    %artifactId%maven-war-plugin%artifactId%
    %version%2.1%version%
    %configuration%
    %webappDirectory%${project.build.directory}/${project.build.finalName}
    %webappDirectory%
    %webResources%
    %resource%
    %directory%src/env/${env}%/directory%
    %resource%
    %webResources%
    %configuration%
    %plugin%

    Also, some where in the top of the pom, add the following:
    %properties%
    %env%test%env%
    %properties%

    Replace % with open or close cone brackets

  8. Jason

    Wow!
    This is exactly what I was looking for.
    Thanks a million .. it works perfectly!!!

  9. Henrik H

    @Suresh Bhaskaran:

    Your modification is a BIG step backwards. It has all the same problems as using profiles have, so you might as well use profiles.

    Let me quote this article you commented on but seemingly did not read:

    “When you have your artifact, how do tell which environment it is configured for? Even worse what if you forget to enable the production profile when releasing or maybe enable the test profile instead. Then you have released a test version of the project and there is no way you can tell except for checking the details in the artifact. Further more you commonly want to deploy the released version to a test environment, but you can not do that out of the box, because the released artifact in the Maven repository is configured for production (if you enabled the right profile). Thus you need to check out the release tag and build a new version with the test profile enabled. Now you end up with two files with the same name and version, that are configured differently. Not a very appealing solution.”

  10. Pulkit

    Hi,

    Very well explained. Worked great. Thanks for the solution.

  11. Shalin Singh

    Thanks Henrik and Suresh

    Your blog and comments help me a million times to handle this situation.

    I want to have same war file name for all environment like ROOT.war so I remove classifier and place each in separate folder.

    Thanks again guys.

  12. Roger Koche

    This solution only works for files that do not need compilation. Without additional configuration java files will simply be transferred as .java files instead of compiled .class files

Leave a Reply