Friday, 17 January 2014

Maven Explanation - part 1


Comparing Maven and Ant, I observed that it is much easier to find developers that feel comfortable in using Ant than Maven. Time cannot be the excuse as Maven has been around and be widely adopted for sometimes. The root cause, from my own opinion, is the differences in design and goals between Maven and Ant.

Background

Ant was born as a Java build tool. It is simpler and easier to use than GNU Make, the build tool for C and C++. Compare to Makefile, Ant file is very readable because it is written in xml format. 

Fundamentally, both Ant and Make are WYSIWYG build tool. You need to specify clearly in the build descriptor file what need to be done and how to do. Most of the times, there is a need to execute more than one step to compile or package your project. This end up causing the build descriptor file long and tedious to write. 

Fortunately, unless for the first project, no one try to write build descriptor file from beginning. Rather, developers only copy/paste and modify the template file to fit other projects.

Slowly, Ant getting more popular and became de-facto build tool for all Java projects. However, Ant still have some problems. Developers often complain that Ant build descriptor file is too verbose and lack of formal structure. Because of this, Maven was introduced as a replacement for Ant. To most of developers, the most obvious benefit of using Maven is the ability to avoid including application libraries to the project but actually, Maven offers much more than that. 

To simplify and standardize thing, Maven introduce the standard lifecycle with some pre-defined steps for project build. However, to shorten the descriptor file, Maven hides most of the steps and only expose a build descriptor file that almost empty. Because of this reason, not all users of Maven have clear understanding of Maven build steps.

Maven build lifecycles

To understand Maven, we must first understand build lifecycles. Maven groups pre-defined steps of a build to 3 lifecycles, 'clean', 'default' and 'site'. The rationale behind this is some steps are interrelated and normally be executed together. 

Maven use the term 'phase' to describe build step. The order of phase execution in one lifecycle is fixed.

As the lifecycle names suggests, Maven lifecycles tackles 3 major concern of developers: clean the build, build and create documentation.

Here are the list of phases for each life cycle:


The amount of included phases for one lifecycle varies from as low as 3 to as high as 22. However, developers only need to remember some important phases rather than memorizing all.

In a standard Maven command, developer need to specify at least one lifecycle to execute. To instruct Maven to execute one lifecyle, developer must choose one phase as  target in mvn command. Because Maven executes phases by order, any phase appears before the chosen phase in selected lifecycle will be automatically executed as well. Obviously, any phase appears after the selected phase in the lifecycle will not be executed. For example, when you type:

mvn clean install What Maven actually need to execute is 

  • pre-clean, clean
  • validate, initialize, generate-sources, process-sources, compile, process-classes, test, prepare-package, pre-integration-test, integration-test, post-integration-test, verify, install


Maven plugins

Plugin and execution

The name os Maven phases may be misleading for beginner. To be precise, phase define the time to do something, not the actual tasks to be done. For developers that have prior experience with Ant, phase in Maven does not equal to goal in Ant. The equals concept of Ant's goal should be plugin execution.

A Maven projects is built with the help of Maven plugins. A plugin declaration in Maven build descriptor file combines of plugin configuration, and at least one executions.

A plugin execution is identified by id, goal and phase. 
  • The id is to identify execution if you register multiple executions of the same plugin to your build descriptor file. 
  • The phase tell Maven which step it should execute the plugin.
  • The goal tell Maven which goal of the plugin that should be executed. One plugin can support multiple goals for different purposes.
  • The execution can have optional configuration. When plugin configuration and execution configuration contains duplicated information, plugin configuration has higher priority. This feature can be helpful sometimes (for example, using a single plugin to start multiple servers).
There are two possible scenarios to trigger a plugin execution. The more popular way is to define plugin execution with a id and phase. In this case, if User trigger a lifecycle and selected phase, the plugin execution is triggered as well. The other way is to trigger plugin execution manually by specifying goal of plugin in Maven command. In this case, developer bypass lifecycle, phase and only execute one single plugin.

It is perfectly possible to migrate Ant project to Maven as Maven provide Ant execution plugin that can execute Ant task.

Default Binding

The tricky part for Maven is some plugin are automatically included in the Maven lifecycle without need for declaring. To makes things more complicated, the plugin executions are included dynamically, depend on project package.

Below is the list of all of the default executions as provided by Maven website

Clean Lifecycle Bindings
clean
clean:clean

Default Lifecycle Bindings - Packaging ejb / ejb3 / jar / par / rar / war

process-resources
resources:resources
compile
compiler:compile
process-test-resources
resources:testResources
test-compile
compiler:testCompile
test
surefire:test
package
ejb:ejb or ejb3:ejb3 or jar:jar or par:par or rar:rar or war:war
install
install:install
deploy
deploy:deploy

Default Lifecycle Bindings - Packaging ear

generate-resources
ear:generate-application-xml
process-resources
resources:resources
package
ear:ear
install
install:install
deploy
deploy:deploy

Default Lifecycle Bindings - Packaging maven-plugin

generate-resources
plugin:descriptor
process-resources
resources:resources
compile
compiler:compile
process-test-resources
resources:testResources
test-compile
compiler:testCompile
test
surefire:test
package
jar:jar and plugin:addPluginArtifactMetadata
install
install:install
deploy
deploy:deploy

Default Lifecycle Bindings - Packaging pom

package
site:attach-descriptor
install
install:install
deploy
deploy:deploy

Site Lifecycle Bindings

site
site:site
site-deploy
site:deploy

Now, let re-visit the example above, when you type:

mvn clean install

It is possible to achieve the same result by manually trigger plugins by typing:

mvn clean:clean
mvn resources:resources
mvn compiler:compile
mvn resources:testResources
mvn compiler:testCompile
mvn surefire:test
mvn jar:jar
mvn install:install

Maven called this default binding. The configuration for default binding is stored in components.xml (Maven 2.x) or default-bindings.xml (Maven 3.x). The default binding is deeply stored in maven-core jar file and you will not be able to change it without re-packaging Maven.

Build Configuration

The plugin can retrieve information from build configuration, plugin configuration and plugin execution configuration. To understand more, you can open any pom file editor that support effective Pom view. Here is the simplified version of the build configuration generated by effective pom view:


  <build>
    <sourceDirectory>maven_webapp\src\main\java</sourceDirectory>
    <scriptSourceDirectory>maven_webapp\src\main\scripts</scriptSourceDirectory>
    <testSourceDirectory>maven_webapp\src\test\java</testSourceDirectory>
    <outputDirectory>maven_webapp\target\classes</outputDirectory>
    <testOutputDirectory>maven_webapp\target\test-classes</testOutputDirectory>
    <resources>
      <resource>
        <directory>maven_webapp\src\main\resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>maven_webapp\src\test\resources</directory>
      </testResource>
    </testResources>
    <directory>maven_webapp\target</directory>
    <finalName>maven_webapp</finalName>
    ...
</build>

Because of this build configuration that you was told to put your source code inside src/main/java, your resource inside src/main/resources and your web app inside src/main/webapp. If you want to override Maven default setting, simply provide different value in the build descriptor file (for example, building a project with both Ant and Maven). However, it should only be used as the last resort because overriding default behaviour of Maven can cause confusion for anyone maintain your project.

Plugin & Execution Configuration
From above explanation, we know that it is possible to choose another source folder rather than default src/main/java by overriding build properties.
There is another way to achieve this by manually configuring plugin behaviour. Here is one example as provided by Maven:
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <includes>
            <include>**/core/**</include>
          </includes>
        </configuration>
      </plugin>
Look at the configuration above, it is even possible to define multiple source folders for a project. As usual, if you want to override Maven default configuration, declare the plugin in pom.xml.
Sometimes, you even need to add more execution and modify existing execution. In this case, providing execution configuration is the only choice. 
In the below example, we want to add one more execution for integration test, which suppose to run only on intergration-test phase. From our requirement, the configuration for the two execution must be different to mutually exclude test cases. Still it can share the common configuration that allow us to enable/disable both executions.
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
             <version>2.12</version>
            <configuration>
                 <skipTests>${skip-all-tests}</skipTests>
             </configuration>
             <executions>
                 <execution>
                    <id>default-test</id>
                    <phase>test</phase>
                    <goals>
                        <goal>test</goal>
                    </goals>
                    <configuration>
                        <skip>${skip-unit-tests}</skip>
                        <groups>unit</groups>                        
                        <excludedGroups>integration</excludedGroups>
                    </configuration>
                </execution>
                <execution>
                    <id>integration-tests</id>
                    <phase>integration-test</phase>
                    <goals>
                        <goal>test</goal>
                    </goals>
                    <configuration>
                        <skip>${skip-integration-tests}</skip>
                        <groups>integration</groups>
                        <excludedGroups>unit</excludedGroups>
                    </configuration>
                </execution>
            </executions>
        </plugin>
Please notice the id of the first execution is default-test. This is because Maven defining a default execution id for each default binding plugin execution.
The name of this implicit id is always default-_____