30. September 2022
10 min

Maven beyond mvn clean install

In this article we would like to explain how build lifecycles work, clear up what exactly "mvn clean install" does and when to use it. Then we want to go into more detail about POMs and project relationships such as inheritance and  dependencies. Finally we will go over a practical example of how all the previous topics come together.

Foreword

Have you worked in Maven projects for years only using the magic “mvn clean install” command but nothing more?
That was our experience until recently until we discovered some of the powerful functionalities Maven offers. We would like to share the ones we found most important and useful with our past selves and whomever is in that place.

In this article we would like to explain how build lifecycles work, clear up what exactly "mvn clean install" does and when to use it.
Then we want to go into more detail about POMs and project relationships such as inheritance and  dependencies.
Finally we will go over a practical example of how all the previous topics come together.

Disclaimer: We are not Maven experts, so if you notice any errors please let us know, we would be very happy to learn from and correct our mistakes.

Command cheatsheet

Are you here just for the quick overview? Here is how to run Maven commands

mvn [options] [<goal(s)>] [<phase(s)>]

and here is a list of Maven commands we found we were using often and did not know about them.

Command
Description
Options
-X
Debug mode
-o
Offline mode
-q
Quiet output, only shows errors
-ff,–fail-fast
Stop at first failure in refactorized builds
Default lifecycle
compile
Compile the source code of the project
test
Test compiled source code without packaging or deploying the code
Help plugin
help:effective-pom
Display the effective POM as an XML for this build
help:effective-settings
Display the calculated settings as an XML for the project
help:active-profiles
List the profiles which are currently active for the build
Dependency plugin
dependency:tree
Display the dependency tree for this project
dependency:analyze
Analyze the dependencies of this project and determine which are: used and declared; used and undeclared; unused and declared.
dependency:analyze-duplicate
Analyze the <dependencies/> and <dependencyManagement/> tags in the pom.xml and determine the duplicate declared dependencies
dependency:purge-local-repository
Tell Maven to clear dependency artifact files out of the local repository

Build Lifecycle

One of the most basic Maven concepts are build lifecycles. You can read all about lifecycles here, but we would like to quickly go over the basics.

A build lifecycle is an explicitly defined sequence of build phases, which define the goals that are to be executed.

Let us explain this. Each build lifecycle is defined by a list of build phases. In turn each build phase defines the list of tasks that are to be executed, these tasks are called plugin goals.

There are three built-in lifecycles in Maven, the default build lifecycle, which we will go over in more detail, the clean lifecycle that cleans the project and the site lifecycle that creates a project web site.

Build Phase

Build phases are the essential building blocks of the build lifecycles. Even though the order in which the different phases are being executed is defined by the lifecycle, here is where most of the fine-tuning happens.

A build phase is composed of a series of plugin goals, which define specific tasks for building or managing a project.

While a build phase is made up of plugin goals, a plugin goal is not necessarily linked to a build phase, actually it can be linked to zero or more than one build phases.

An example of a plugin goal is the dependency:tree, which is the tree goal of the dependency plugin. This goal can be executed on it’s own, displaying the dependency tree of the current project.

Now let’s redefine the build phase. A build phase is made up of a list of zero or more plugin goals that will be executed in order.

Plugins

As you might imagine if you got so far, in Maven most work is done by executing plugins.

Each plugin can be viewed as a group of plugin goals that can be executed in the build phase they are bound to.

For example maven-compiler-plugin is a plugin comprised of two plugin goals that will be executed in the corresponding build phase they are bound to. The compiler:compile goal will be run in the compile phase while the compiler:testCompile goal will be run during the test-compile phase.

maven-compiler-plugin

  • compiler:compile is bound to the compile phase and is used to compile the main source files
  • compiler:test compile is bound to the test-compile phase and is used to compile the test source files

Basically what Maven does is execute plugin goals, sometimes they are packaged neatly in plugins such as when we run mvn clean install, other times they are explicitly defined and fine-tuned in the POM file.

Default Lifecycle

Now that we have gone over the concepts of build lifecycle, build phases, plugins and plugin goals, let’s go over the default lifecycle in some more detail.

The default lifecycle is set up by the following phases:

  • validate – validate if the project is correct and if all necessary information is available.
  • compile – compile the source code of the project.
  • test – test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed.
  • package – take the compiled code and package it in its distributable format, such as a JAR.
  • verify – run any checks on results of integration tests to ensure quality criteria are met.
  • install – install the package into the local repository, for use as a dependency in other projects locally.
  • deploy – done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.

This means that if we would use the default build lifecycle, the phases above would all be used in order. Maven would then first validate the project,  compile it, test it, and so on.

mvn clean install

You probably noticed that we mentioned the clean lifecycle and the install build phase so far. As you probably already know, when we run “mvn clean install” we clean our project and then rebuild it. What is actually taking place is the following:

  1. We run the clean lifecycle, which is made out of the plugin goal clean:clean and which takes care of cleaning the project.
  2. We then run install. What this does is run the build phases of the default lifecycle and  for each build phase all the plugin goals they contain are executed.
    1. validate,
    2. compile,
    3. up until it runs the install phase.
So if you just want to compile your project you no longer need to run “mvn install” since “mvn compile” would suffice and be faster as you skip the test, package, verify and install phases. Similarly if you want to run the unit tests “mvn test” would do the trick.
One last note regarding lifecycles. As mentioned there are three built-in lifecycles in Maven, while one could define new lifecycles or build phases to add to an existing one, it is more common to use the default lifecycles and configure their behaviour by adding the desired plugin goals.

POM

So after realising how lifecycles work we had to have a deeper look at POMs. Project Object Model (POM) is an XML file that contains the configuration to build the project and further information. This is where project dependencies, plugins and goals that can be executed and build profiles are defined and configured.

The POM, being an XML file, uses <tags> and uses a type of coordinates to uniquely identify a Maven project: groupId:artifactId:version

  • groupId is generally used to uniquely group an organization’s projects under the same grouping.
  • artifactId is generally the name of the project, and groupId:artifactId uniquely identifies a project.
  • version defines the exact variant of the project groupId:artifactId.

These fields are required to define a new <project> in a POM. And this would be the minimum POM one could use.

Super POM

As described above we could have a POM that just defines the project name and version, but even such a project can be built with Maven, and here is how. The super POM. This is a POM that all POMs inherently extend (much like the Object class in java) and carries a lot of the basic Maven functionality with it, unless you explicitly decide not to do so.

POM Relationships

One very important point we just presented above is that a POM can extend other POMs. The model inheritance is just one of the possible project relationships such as:

  • Inheritance
  • Aggregation
  • Dependencies

Inheritance

A parent model can be defined in your POM file using the <parent> tag.

Elements in the parent POM that are merged with your POM are the following:

  • Dependencies
  • Developers and contributors
  • Plugin lists (including reports)
  • Plugin executions with matching ids
  • Plugin configuration
  • Resources

In order to visualise resulting POM one can use the help:effective-pom plugin goal.

Aggregation

Another way to relate projects is by grouping them together using the <modules> tag. This allows us to group different projects to be executed as a group in a single POM without the need to consider the inter-module dependencies.

Dependencies

This is probably the most commonly used feature of Maven, and one could argue the most useful one. Under the <dependencies> tag we can delegate the management of the list of dependencies a project uses to Maven. Maven downloads and links the dependencies on compilation.

To add a dependency use the <dependency> tag and define the project you would like to add by using it’s Maven coordinates groupId:artifactId:version.

Transitive Dependencies

As we just saw adding a dependency is pretty straightforward, but what happens with the dependencies of these dependencies? These transitive dependencies are included by Maven automatically, which takes care of the overhead of managing all those dependencies.

Unfortunately automatically handling transitive dependencies has it’s drawbacks, namely cyclic dependencies. This problem and all possible solutions are described in detail here but we want to give you a quick overview. The problem arises when our project depends (indirectly) on more than one versions of the same artifact and which of those Maven should choose.

The default solution Maven uses is dependency mediation, prioritising the dependencies that are closest to our project in the dependency tree. In the following example the dependency D:1.0 would be used as the path A->E->D is shorter than A->B->C->D.

Keeping this default solution in mind, it is often preferred to explicitly handle which version of a dependency should be used. There are different ways of doing this such as excluding unwanted dependencies or using optional dependencies as described in the documentation. We would like to go into more detail of some of them.

Dependency Version Requirement Specification

We were so used to define the version of a dependency using the soft requirement syntax below (1.0.0) that we were surprised to find out there are quite a few different ways to define a dependency version:

  • 1.0: Soft requirement for 1.0. Use 1.0 if no other version appears earlier in the dependency tree.
  • [1.0]: Hard requirement for 1.0. Use 1.0 and only 1.0.
  • (,1.0]: Hard requirement for any version <= 1.0.
  • [1.2,1.3]: Hard requirement for any version between 1.2 and 1.3 inclusive.
  • [1.0,2.0): 1.0 <= x < 2.0; Hard requirement for any version between 1.0 inclusive and 2.0 exclusive.
  • [1.5,): Hard requirement for any version greater than or equal to 1.5.
  • (,1.0],[1.2,): Hard requirement for any version less than or equal to 1.0 than or greater than or equal to 1.2, but not 1.1. Multiple requirements are separated by commas.
  • (,1.1),(1.1,): Hard requirement for any version except 1.1; for example because 1.1 has a critical vulnerability. Maven picks the highest version of each project that satisfies all the hard requirements of the dependencies on that project. If no version satisfies all the hard requirements, the build fails

Dependency Scope

One option available to limit the transitivity of a dependency is to define it’s scope, defining when each dependency should be used based on the tasks being executed. There are five scopes available:

  • compile – this is the default scope, used if none is specified. Compile dependencies are available in all classpaths. Furthermore, those dependencies are propagated to dependent projects.
  • provided – this is much like compile, but indicates you expect the JDK or a container to provide it at runtime. It is only available on the compilation and test classpath, and is not transitive.
  • runtime – this scope indicates that the dependency is not required for compilation, but is for execution. It is in the runtime and test classpaths, but not the compile classpath.
  • test – this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases. It is not transitive.
  • system – this scope is similar to provided except that you have to provide the JAR which contains it explicitly. The artifact is always available and is not looked up in a repository.

Dependency management

Another way to control the version of the dependency added is to use a bill of materials (BOM). By adding a dependency in the <dependencyManagement> list we define which version of that artifact should be used if and when these are encountered in transitive dependencies or dependencies without a version. There is a lot of power hidden under this simple tag and we would like to give you an example of how we used it in our project.

Putting the pieces together

So far we have given you a quick glimpse of the different pieces of the puzzle, but we would like to present how these were all put together in our project for an elegant way of handling dependencies across different microservices. In our project we had to handle a lot of independent projects, each being a service, but a lot of them required a common set of dependencies. There were quite a few of them but just to illustrate this let’s assume this was one spring boot starter. Our first thought was to add this dependency to each project. We guess that after reading this article you can imagine that this was not the best solution. So what we did was the following:

Parent POM

We created a new POM to be extended by the individual projects. We could have added all the dependencies to that parent POM and have them imported by each project, but we since not all of them would need all of the dependencies we went a different way.

Using a BOM

What we wanted was all of the different projects to use the same dependencies, but only if they needed them. So instead of adding the dependencies in the parent POM we added them in a list of dependencies management, creating a bill of materials. We had basically created a wishlist for the dependency versions. Our projects extended this parent POM and in each one we added the dependencies we wanted to include without needing to specify the versions in each project as they were using the ones defined in the parent POM.
We found this to be a very elegant solution to manage the versions of commonly used dependencies in a centralised manner, without having to add all the dependencies to all projects if not necessary. We could go one step further and do both, that is add the absolute basic dependencies that would be shared by all projects in the parent POM so that we would not need to add them in all projects individually.

Final Thoughts

Is this all that Maven has to offer? Definitely not, but after working with some of the functionalities we described above we came to the realisation of all the potential hidden behind the commonly used ‘mvn clean install‘ command. We hope we could give you some insights and we managed to peak your interest to investigate a bit further! If we did, we urge you to have a deeper look at the official documentation as it gives a lot more insight. Finally, there are a lot of other interesting topics for further reading such as build profiles, when using settings depending on the environment where it is being built is required, and properties, to use as value placeholders. Have fun discovering all the hidden power of Maven!

References:

Introduction to the lifecycle

POM

Transitive Dependencies


Image Sources: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Transitive_Dependencies

Comment article