Sunday, July 3, 2011

Bending the Spoon: How to build your Grails application with Maven

It Hurts When I Do That...

When starting a new project a few years back, we made the decision to transition from custom Ant scripts to using Maven as our build manager.  This project contained standard Java libraries (JAR files), OSGi bundles, Grails plugins, and of course, a Grails application (WAR file).  We toyed with the idea of just calling our Ant scripts from Maven, but as engineers, we felt that was a cop-out and defeated the purpose of transitioning to a convention-over-configuration build system such as Maven.  However, in order to achieve the transition to Maven we wanted to be able to completely build our Grails application from Maven without using the Grails command-line (this also meant not taking the easy way out and just using the Maven "antrun" plugin to invoke Grails commands).  Along the way, we had to resolve various issues pertaining to conflicts in conventions between Grails and Maven.  This first article will walk through the Maven support built in to Grails, including the Grails Maven plugin and how we achieved complete Grails/Maven integration for our project.  Subsequent articles will focus on "Mavenizing" your Grails plugins and treating them like any other Maven dependency when building your Grails application.  For the purposes of this article, assume Grails 1.3.7 and Maven 2.2.1 (however, the steps could be applied to any version of Grails and Maven 2.x or 3.x).

Know Your Build Needs

The first step when choosing how to build your Grails application is to spend some time familiarizing yourself with the build support in Grails.  At its core, Grails provides dependency management options in combination with build management and packaging scripts out of the box.  These options can be broken down into the following categories:

  • Simple command line build with few or no external dependencies 
  • Command line build with repository managed dependencies 
  • Command line build with Maven managed dependencies (presence of a POM file) 
  • Maven build (build via Maven using Grails Maven plugin, not the Grails command line)

Build Management

The simple build approach is to let Grails manage everything by placing any required third-party library JAR’s in the /lib folder of your Grails application and using the Grails command line tools to build, package and/or run your application or plugin.  The Grails command line tools make use the Grails Gant scripts, which can be found in the $GRAILS_HOME/scripts folder of your Grails installation.  Building your Grails application or plugin is as simple as running:

grails <environment> war
or
grails package-plugin
where <environment> is one of  "dev", "test", or "prod".  More "advanced" users can make use of the built-in dependency resolution options, as described below.

Dependency Resolution

It is important to realize that Grails uses Ivy to manage the resolution of any dependencies required by plugins or listed in your BuildConfig.groovy file.  Ivy’s role is simply to resolve dependencies (libraries) and get them on the classpath and into your WAR/ZIP file when building your application/plugin via the Grails command line tools.  You can declare dependencies (with Maven-style coordinates) in your application’s BuildConfig.groovy file:


    grails.project.dependency.resolution = {
        ...
        dependencies {
            runtime 'com.mysql:mysql-connector:5.1.5'
        }
    }


You also need to be aware that plugins may include JAR's included in its /lib directory or a Dependencies.groovy file that defines libraries required by the plugin when it is installed in your application (this will be very important later when we discuss "Mavenizing" your Grails plugins).  Finally, Grails also supports pointing Ivy at Maven repositories to resolve dependencies in your BuildConfig.groovy file:


    grails.project.dependency.resolution = {
        repositories {
            mvnRepo "http://repo.grails.org/grails/core"
        }
        ...
    }


This is useful when you need to resolve an internal dependency library when prototyping or building a simple application.  If you are developing in an environment where a POM file full of required dependencies and repositories has been provided for you, you can use the pom true option in your BuildConfig.groovy to make the Grails scripts resolve dependencies from the provided pom.xml file in the root of your project:


    grails.project.dependency.resolution = {
        pom true
    }


This option causes the Grails build scripts to look at your POM file for dependency resolution.

Grails Maven Integration

Up to this point, we have focused on looking at the built-in build support in Grails via the Gant scripts and dependency management mechanisms exposed via the BuildConfig.groovy file.  In addition to its “native” build support, Grails also supports building plugins and applications fully via Maven (with some extra TLC).  The Grails core development team maintains a Grails Maven plugin, which provides Maven goals to perform various Grails related tasks (documentation for the plugin can be found here).  It is important to recognize from the beginning that the Grails Maven plugin is essentially a facade around the Grails Gant scripts.  This means that at the point where the Grails Maven plugin begins to execute a goal, control is transferred from Maven to the Grails scripts themselves.  This detail is probably something that is normally considered part of the "black box" and not given much thought, but it is something that gave us a lot of pain at first.  For instance, because the Grails scripts are essentially being "executed" by the Grails Maven plugin, any libraries that the scripts require to run must be placed on the classpath as runtime dependencies.  This means that you must include dependencies in your POM file that are not actually required by your application at runtime (there have been numerous JIRA issues opened regarding this exact issue in the past).  Because it is a wrapper about the Grails scripts, it is also imperative to make sure that the conventions match.  There are four properties in the BuildConfig.groovy that can be set to cause the Grails scripts to produce artifacts in same directories that Maven expects to find its build artifacts:


    grails.project.class.dir = "target/classes"
    grails.project.test.class.dir = "target/test-classes"
    grails.project.test.reports.dir = "target/test-reports"
    grails.project.war.file = "target/${appName}-${appVersion}.war"


The above settings are the default values included in the BuildConfig.groovy file when you generate your project (the "grails.project.war.file" property will be commented out by default). 

Where To Start

The first decision that we made was to create a POM file that would encapsulate all of the Grails dependencies required by our application.  While the Grails Maven plugin provides a goal to create a POM from their archetype (create-pom), I would recommend that you start by hand-rolling your application or plugin's POM file.  The main reason for this is that the dependencies in Grails 1.3.x are a mess and the archetype produces a somewhat incorrect/out-of-date POM file.  By encapsulating these dependencies in one POM file, it would allow us to easily change the dependency set when and if we decided to upgrade the version of Grails used in our application (we were able to upgrade successfully from 1.2.0 to 1.3.7 using this method with minimal changes to our project's POM file).  This POM file looks something like this:


    <?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>dependencies</groupId>
        <artifactId>grails</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>pom</packaging>

        <dependencies>
            <dependency>
                ....
            </dependency>
        </dependencies>

        ...
    </project>


This is just a simple POM that will cause Maven to pull in all of the Grails dependencies required to build our application or plugin.  It is recommended that you leave the version as a SNAPSHOT to make it easier for you to change the version of Grails or the included dependencies without having to redeploy the POM's for your projects that depend on it.  Through some trial and error of attempting to build a simple Grails application WITHOUT any artifacts (i.e. no controllers, domain classes, etc), we arrived at the initial set of dependencies:

  • org.grails:grails-bootstrap:1.3.7
  • org.grails:grails-core:1.3.7
  • org.grails:grails-crud:1.3.7
  • org.grails:grails-gorm:1.3.7
  • org.grails:grails-scripts:1.3.7
  • net.sf.ehcache:ehcache-core:1.7.1
  • hsqldb:hsqldb:1.8.0.10
  • org.slf4j:slf4j-log4j12:1.5.8 (required by Grails scripts to execute)

The majority of the above dependencies will be required by your application a runtime.  As mentioned earlier, a few of these dependencies are merely required for the Grails scripts to execute properly (more on how to resolve this issue in a bit).  Once you have your Grails dependency POM file created, install it into your local Maven .m2 repository:
mvn install
The next step for us was to figure which dependencies we needed to be included in this pom.  To figure this out, we created a simple Grails application using the Grails command-line tools: 
grails create-app test-app
Once we had created the skeleton project, we packaged the application into a WAR file so that we could see which dependencies the Grails build scripts pull into the WAR:
grails prod war
This produces a WAR file in the "target" directory of the Grails application, which we set aside for later comparison to the WAR file produced by Maven.  Next, in order to built a WAR using Maven for the same test project, we created a new POM file for the test project:


    <?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>my-company</groupId>
        <artifactId>test-app</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>grails-app</packaging>

        <dependencies>
            <dependency>
                <groupId>dependencies</groupId>
                <artifactId>grails</artifactId>
                <version>1.0-SNAPSHOT</version>
                <type>pom</type>
            </dependency>
        </dependencies>

        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>2.4.1</version>
                </plugin>
                <plugin>
                    <groupId>org.grails</groupId>
                    <artifactId>grails-maven-plugin</artifactId>
                    <version>1.3.7</version>
                    <extensions>true</extensions>
                    <configuration>
                        <nonInteractive>true</nonInteractive>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>


Note that if you do not modify the "app.version" property in the application.properties file, Maven will fail the "validate" phase, complaining that the version numbers do not match.  The simple fix for this is to ensure the version number in the application.properties file matches the version number in your POM file (i.e. 1.0-SNAPSHOT).  Also note the special packaging type for this application ("grails-app").  Finally, notice that we added a configuration block for the Grails Maven plugin to execute grails with the "non-interactive" flag, so that we will not be prompted to enter "Y" during the build, if required.  Once we havd created and saved the pom.xml file to the root of the test Grails project, we built the WAR file with Maven by running: 
mvn clean package
This will produce a WAR file in the "target" directory of your Grails project.  The next step was to extract both WAR files (the one produced by Grails command-line tools and the one built with Maven) to temporary directories in order to compare their contents.

We Don't Need No Stinkin' Dependencies!

Once the contents of the two WAR files had been extracted to different directories, we used a merge/diff tool that can compare directories to look for the differences in the WEB-INF/lib folder in the WAR.  The comparison will tell you a few different things:

  • Libraries that appear ONLY in the WAR produced by Maven need to be EXCLUDED from the Grails dependency POM file we created earlier, DELETED from the WAR by using a trick within the BuildConfig.groovy file OR matched up against a dependency of the same name but different version number and reconciled (i.e., Grails will pull in a different version of the Log4j library than Maven).
  • Libraries that appear ONLY in the WAR produced by Grails should be considered to be missing dependencies in the Maven build and need to be ADDED to the Grails dependency POM file we created earlier.
  • The Spring dependencies pulled in by Grails and Maven are identical, but the JAR files are named differently (the Grails versions are named "org.springframework.aop-3.0.5.RELEASE.jar", while the Maven ones are named "spring-aop-3.0.5.RELEASE.jar").  Verify that all the names match (i.e. "aop", "asm", etc).  Follow the above two bullet points for which need to be included/excluded.

To resolve the dependency soup that you see in the WAR's, follow these steps: 

  1. Add any missing dependencies (those that are in the Grails command-line WAR ONLY) to the Grails dependency POM.
  2. Rebuild the WAR file via Maven.
  3. Re-compare the WAR produced by Maven to the WAR produced by Grails.
  4. Look for duplicate libraries that have different version numbers.  Exclude the conflicts from the Grails dependency POM file and add the correct version dependency to the Grails dependency POM file.
  5. Rebuild the WAR file via Maven.
  6.  Add logic to the project's BuildConfig.groovy to delete the non-runtime dependencies pulled in just for executing the Grails scripts (more on how to do this in a bit).
  7. Rebuild the WAR file via Maven.
  8. Re-compare the WAR's -- the included libraries should now be identical.  If not, repeat until they are identical)

To see the Maven dependency tree, use the following Maven command from the root of your Grails project:
mvn dependency:tree -Dverbose=true

This will output the resolved dependency tree for your project and will help you see how the dependency are being transitively resolved and pulled into the WAR file (I recommend piping this to a file, as the output can be rather long).  The "verbose" flag tells the goal to also print out conflicts, so you can see if two dependencies are pulling in different versions of the same dependency and how/why you are ending up with the dependency in the WAR file (based on Maven's conflict resolution strategy).  It is also recommended that you clean up these conflicts by excluding the dependency that you do not want in your dependency tree.  To exclude dependencies in your POM file, find the parent dependency and add the following block to it:


    <exclusions>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
    </exclusions> 


Note that the following artifacts will appear only in the Maven WAR and should NOT be excluded via your Maven dependencies POM file (we will take care of removing these later):

  • org.apache.ant:ant:1.7.1
  • org.apache.ant:ant-launcher:1.7.1
  • org.apache.ant:ant-junit:1.7.1
  • org.apache.ant:ant-nodeps:1.7.1
  • org.apache.ant:ant-trax:1.7.1
  • org.grails:grails-docs:1.3.7
  • org.grails:grails-scripts:1.3.7
  • org.grails:grails-test:1.3.7

As mentioned earlier, because of how the Grails Maven plugin invokes the Grails scripts to build the application, some dependencies are required just to run the underlying Grails scripts (listed above).  These dependencies are not required to deploy your WAR (unless you introduce some specific runtime dependency on them).  To clean out any libraries that you do not want in your WAR (such as the ones listed above), you can make use of the resources closure in the BuildConfig.groovy file:


    grails.war.resources = { stagingDir, args ->
        delete(file:"${stagingDir}/WEB-INF/lib/ant-1.7.1.jar")
    }


This closure gets executed by Grails right before it packages up the WAR file.  It calls the Gant "delete" task to remove the file from the staging directory prior to creating the WAR archive.  Simply add all of the libraries that you do not want included in the WAR (a complete list of what needs to be deleted is listed towards the end of this article).  You can even make it a super-set of files to delete, as it should not cause the build to fail if you reference a file that is sometimes included (this is great if you are using profiles in your Maven build to include different dependencies depending on the selected profile).

Putting It All Together

Once we had identified all of the dependencies that needed to be excluded/deleted, it was just a matter of modifying both the project's BuildConfig.groovy file and the Grails dependency POM we created earlier.  Below is a list of all of the files that need to be deleted in order get the two WAR files in sync:

  • ant-1.7.1.jar
  • ant-junit-1.7.1.jar
  • ant-launcher-1.7.1.jar
  • ant-nodeps-1.7.1.jar
  • ant-trax-1.7.1.jar
  • bcmail-jdk14-138.jar
  • bcprov-jdk14-138.jar
  • core-renderer-R8.jar
  • gant_groovy1.7-1.9.2.jar
  • gpars-0.9.jar
  • grails-docs-1.3.7.jar
  • grails-scripts-1.3.7.jar
  • grails-test-1.3.7.jar
  • itext-2.0.8.jar
  • ivy-2.2.0.jar
  • jsr166y-070108.jar
  • junit-4.8.1.jar
  • radeox-1.0-b2.jar
  • servlet-api-2.5.jar
  • svnkit-1.2.3.5521.jar

After all inclusions and exclusions, the Grails dependency POM file should contain the following dependencies (with exclusions):

  • org:grails:grails-bootstrap:1.3.7
  • org.grails:grails-core:1.3.7
    • Exclusion: commons-beanutils:commons-beanutils
    • Exclusion: commons-collections:commons-collections
    • Exclusion: commons-digester:commons-digester
    • Exclusion: commons-pool:commons-pool
    • Exclusion: javax.persistence:persistence-api
  • org.grails:grails-crud:1.3.7
  • org.grails:grails-gorm:1.3.7
  • org.grails:grails-scripts:1.3.7
  • org.aspectj:aspectjweaver:1.6.8
  • commons-beanutils:commons-beanutils:1.8.0
    • Exclusion: commons-logging:commons-logging
  • commons-codec:commons-codec:1.4
  • commons-collections:commons-collections:3.2.1
  • commons-pool:commons-pool:1.5.5
  • net.sf.ehcache:ehcache-core:1.7.1
  • hsqldb:hsqldb:1.8.0.10
  • jstl:jstl:1.1.2
  • log4j:log4j:1.2.16
  • org.slf4j:slf4j-log4j12:1.5.8
    • Exclusion: log4j:log4j
  • taglibs:standard:1.1.2

We now had a Grails dependency POM that would help us build a WAR of our application using Maven and not the Grails command line.  This POM, in combination with the modifications to BuildConfig.groovy to remove unnecessary dependencies, produced a WAR that contains the exact same runtime dependencies as the Grails command-line tools.  The only other piece that we added to our project POM file to make the results more Maven friendly was the use of the Antrun plugin (ugh, I know) to rename the WAR to drop the version number:


    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>1.6</version>
        <configuration>
            <tasks>
                <move file="${project.build.directory}/${project.artifactId}-${project.version}.war" tofile="${project.build.directory}/${project.artifactId}.war" />
            </tasks>
        </configuration>
    </plugin>


But Wait...There's More!

Once we had the capability to build a skeleton Grails project with Maven, we began the task of de-conflicting all the other libraries being resolved by Maven when we started adding our code (and other internal libraries) to the project.  Our friend in this battle was the Maven dependency plugin and use of the "tree" goal (described earlier).  This approach worked for us when building against Grails 1.2.0 and 1.3.7.  Early indications are that a similar approach will work with Grails 2.0 (formerly known as 1.4.x).  Keep in mind that the solution presented above does not come without its fair share of hacks (like making use of the BuildConfig.groovy file to remove dependencies injected via Ivy, etc).  However, as long as Grails has support for Ivy built in to its build infrastructure to resolve dependencies, these workarounds will be necessary when attempting to build a Grails application or plugin with Maven.  Finally, I have uploaded the complete sample Grails dependency POM file and the complete sample Grails application POM file:  Grails POM Files.  In my next post, I will take a look at how to get your Grails plugins to be resolved as Maven dependencies when building your application via Maven.  Our solution involved some of the tricks that you have already seen in this post and some custom Maven plugin work.  While our approach ultimately gave us true Maven build support in regards to Grails plugins, my goal is to make the current Grails Maven plugin work out of the box in regards to plugins so that we do not need to create a custom plugin just to help with them, but that is a story for another time.

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Jonathan,
    A good article to explain the conflicts between maven & grails on 'convention over configuration'.
    I've been having sleepless nights here to get the current grails application working with maven2 plugins(war, ear and release). A j2ee project source and artifacts i can successfully release into nexus repository. I'm unable to get the correct POM built for existing grails app.

    I'm wondering you can provide me your working pom.xml file for reference..
    Thanks
    Aus rayaku (aus_rayaku@yahoo.com)

    ReplyDelete