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.
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.
Quiet output, only shows errors
Stop at first failure in refactorized builds
Compile the source code of the project
Test compiled source code without packaging or deploying the code
Display the effective POM as an XML for this build
Display the calculated settings as an XML for the project
List the profiles which are currently active for the build
Display the dependency tree for this project
Analyze the dependencies of this project and determine which are: used and declared; used and undeclared; unused and declared.
Tell Maven to clear dependency artifact files out of the local repository
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 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.
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.
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.
compiler:compileis bound to the compile phase and is used to compile the main source files
compiler:testcompile 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.
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:
- We run the
cleanlifecycle, which is made out of the plugin goal
clean:cleanand which takes care of cleaning the project.
- 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.
- up until it runs the
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.
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:
groupIdis generally used to uniquely group an organization’s projects under the same grouping.
artifactIdis generally the name of the project, and
groupId:artifactIduniquely identifies a project.
versiondefines the exact variant of the project
These fields are required to define a new
<project> in a POM. And this would be the minimum POM one could use.
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:
A parent model can be defined in your POM file using the
Elements in the parent POM that are merged with your POM are the following:
- Developers and contributors
- Plugin lists (including reports)
- Plugin executions with matching ids
- Plugin configuration
In order to visualise resulting POM one can use the
help:effective-pom plugin goal.
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>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
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
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.
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
Using a BOM
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!