Maven 101 - Basics

Maven 101 - Basics

·

11 min read

Introduction

Maven is used to manage and build any Java-based project. It provides an opinionated way of the project structure, managing dependencies, building projects and deploying the artifacts.

Project Object Model (POM)

POM stands for Project Object Model. This is the core of Maven and it is declared using XML.

Let's take a look at a sample pom.xml.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- Required -->
    <modelVersion>4.0.0</modelVersion>

    <!-- This 3 properties also known as Coordinates -->
    <groupId>com.bwgjoseph</groupId>
    <artifactId>maven-101-basic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- Optional -->
    <name>maven-101-basic</name>
    <description>Maven 101 Basics</description>

</project>

It contains the following key components:

- modelVersion
- Coordinates
  - groupId, artifactId, version
- name
- description

pom file always exists at the root of the project directory.

root/
├── .mvn
├── src/
│   ├── main
│   └── test
├── mvnw
├── mvnw.cmd
└── pom.xml

Simple POM

The above is what is normally referred to as the Simple POM.

modelVersion and Coordinates are the two minimum declarations required for a pom file to work. And it inherits all of the Super POM declarations by default.

Super POM

Maven comes with Super POM as the default POM which defines all the default configurations of a project. As mentioned above, every project POM extends Super POM, and thus, inherits all the configuration.

A sample of Super POM can be found here. It contains the following

- modelVersion
- repositories
- pluginRepositories
- build
- reporting
- profile

Effective POM

This is the final actual POM after merging the values with Super POM.

We can see the final or effective POM by running the command below

./mvnw help:effective-pom

The output is pretty long so I will not be attaching the logs here, but if you do run it. You will notice that it is a combination of Super and Simple POM.

This is useful for debugging when you want to know what exactly forms up your POM, especially in a fairly complex project.

Goal and Phase

I believe the best place to understand it is through official Maven Docs. So make sure to run through those. Here, I will add some additional notes that aid me in my understanding.

Lifecycle

This is a bird's eye view of the Maven Lifecycle. To see the complete lifecycle view, refer to the complete lifecycle.

Phase

At each lifecycle, it comes with a whole set of the phases that it runs. For example, for clean, it has pre-clean, clean and post-clean.

What's important to note is that build lifecycle needs to run on its own, but default lifecycle should run based on what you want to achieve as it will be (and I quote) "executed in the order given up to the point of the one specified".

./mvnw [phase]

For example, if I want to ensure my project can compile, I can either run ./mvnw clean compile or ./mvnw compile.

Running ./mvnw compile will not trigger clean phase unlike running compile which will ensure validate phase is run first.

./mvnw compile

❌ clean
✔ validate > compile
❌ clean > validate > compile

./mvnw clean compile

✔ clean
✔ validate > compile
❌ clean > validate > compile

What happens in each phase

  • validate: validate the project

  • compile: compile source code

  • test: test the compiled source code

  • package: take the compiled source code and package it into JAR

  • verify: verify package is valid

  • install: install the package into the local repository

  • deploy: deploy the package into the remote repository

Goal

Up to this point, I have only talked about phase. What about goal? It is important to note that for each phase, there will usually be at least one goal (tasks) that is attached which is defined by Maven plugin.

Refer to goal-binding to know each phase is bound by which goal

Remember when I mentioned about phase where running one will automatically invoke the previous phases? If you run a particular goal, it will not invoke any previous phase.

./mvnw [plugin]:[goal]

What it means is that if I run ./mvnw package, it will run the following phase in order

validate > compile > test > package

But if I run ./mvnw jar:jar, it will execute only the jar goal and not anything before that. So if you don't have necessary resources (i.e./target/classes, etc.), it will throw a warning

> ./mvnw jar:jar
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.bwgjoseph:maven-101-basic >--------------------
[INFO] Building maven-101-basic 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jar:3.3.0:jar (default-cli) @ maven-101-basic ---
[WARNING] JAR will be empty - no content was marked for inclusion!
[INFO] Building jar: Z:\Development\workspace\github\bwgjoseph\tutorials\maven-101-basic\target\maven-101-basic-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.682 s
[INFO] Finished at: 2023-09-03T16:05:46+08:00
[INFO] ------------------------------------------------------------------------

Plugin

Maven relies on plugin to perform any type of work. Take a look here for the list of plugins provided by Maven.

Each plugin provides at least 1 goal to it and has no upper-bound limit (AFAIK).

To understand what goals are available, it is usually described in the various help plugin docs, and compiler plugin docs.

One could also use the help plugin to list down in the console.

# using groupId with details
./mvnw help:describe -DgroupId="org.apache.maven.plugins" -DartifactId=maven-clean-plugin -Ddetail=true
# using groupId with without details
./mvnw help:describe -DgroupId="org.apache.maven.plugins" -DartifactId=maven-clean-plugin
# using plugin with version and details
./mvnw help:describe -Dplugin="org.apache.maven.plugins:maven-clean-plugin:3.3.1" -Ddetail=true
# using plugin without version and details
./mvnw help:describe -Dplugin="org.apache.maven.plugins:maven-clean-plugin"

Binding plugin goal to phase

If you want to bind a plugin goal to a particular phase, you can do it via the execution tag even if the plugin has already bound to some phase.

Let's use spotless-maven as an example.

<plugin>
  <groupId>com.diffplug.spotless</groupId>
  <artifactId>spotless-maven-plugin</artifactId>
  <version>${spotless.version}</version>
  <configuration>
    <!-- omitted -->
  </configuration>
</plugin>

As mentioned in the docs, a spotless:check is bound to verify the maven phase, and it means that when running ./mvnw verify it will trigger spotless:check goal. If it was not bounded by the plugin, I would have to write something like this

<plugin>
  <groupId>com.diffplug.spotless</groupId>
  <artifactId>spotless-maven-plugin</artifactId>
  <version>${spotless.version}</version>
  <configuration>
    <!-- omitted -->
  </configuration>
  <executions>
    <execution>
      <phase>verify</phase>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>

And if I want to bound to other phases like compile

<plugin>
  <groupId>com.diffplug.spotless</groupId>
  <artifactId>spotless-maven-plugin</artifactId>
  <version>${spotless.version}</version>
  <configuration>
    <!-- omitted -->
  </configuration>
  <executions>
    <execution>
      <id>spotless-compile-check</id>
      <phase>compile</phase>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Optionally, I can also give it a unique id which is used to identify (and show) in the console.

> ./mvnw compile
// omitted
[INFO] --- spotless:2.39.0:check (spotless-compile-check) @ maven-101-basic ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.429 s
[INFO] Finished at: 2023-09-03T16:13:55+08:00
[INFO] ------------------------------------------------------------------------

Dependencies

One of the most important parts of using build tools like Maven is to allow it to manage the dependencies of the project. Similar to how other build tools such as npm do it as well.

One would specify the dependencies (and version) the project requires, and Maven will download and manage it according to how you defined it.

Below is an example of adding Lombok dependency on the project.

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.28</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

There is a lot more to this such as

  • classifier

  • optional

  • version requirement

  • exclusion

But I will not be covering those. Refer to here if you are interested to know more. In most cases, all those are not required (including scope), only groupId, artifactId, and version are required.

Model Interpolation

Maven provides a way to replace/define value via ${...} syntax. Some of the more common ones are

${project.version}
${project.basedir}
${user.home}
${java.home}
${env.PATH}

This allows us to define it like this

<groupId>com.bwgjoseph</groupId>
<artifactId>maven-101-basic</artifactId>
<version>${project.version}</version>

Model Properties

If I define it under <properties> tag in the pom.xml, I can use it. This is especially useful and commonly used for defining dependency/plugin versions.

<properties>
    <java.version>17</java.version>
    <spotless.version>1.2.3</spotless.version>
</properties>

<plugin>
  <groupId>com.diffplug.spotless</groupId>
  <artifactId>spotless-maven-plugin</artifactId>
  <version>${spotless.version}</version>
</plugin>

When working with Spring Boot project, it is also quite common for us to override the dependency version in <properties> tag like

<properties>
    <java.version>17</java.version>
    <!-- Override dependency version -->
    <lombok.version>1.18.24<lombok.version>
</properties>

Since Lombok dependency is declared and managed via spring-boot-starter-parent, there is no need to re-declare the dependencies in our pom.xml except to declare to the version.

Maven Wrapper

Traditionally, to run mvn command, it is required to download Maven binary, extract, define environment variable, etc. This makes the developer experience less ideal, and more involved.

Maven wrapper was originally a 3rd party project until quite recently that it was officially released as part of Apache Maven Project

Using Maven Wrapper provides an easy way to build any Maven project without having to pre-install Maven on the machine. It has a few key components.

Maven Wrapper Jar

Maven Wrapper Jar is responsible for downloading (then installing) of Maven binary, specified in /.mvn/wrapper/maven-wrapper.properties under distributionUrl. And to invoke the targetted maven distribution whenever a command is triggered.

The maven binary will be downloaded, and extracted to %USERPROFILE%/.m2/wrapper/dists (Windows) or ~/.m2/wrapper/dists (Linux)

Maven Wrapper Distribution

Maven Wrapper Distribution provides

  • mvnw

  • mvnw.cmd

  • .mvn/wrapper/maven-wrapper.properties

When invoking mvnw[.cmd], its job is to trigger/download maven-wrapper depending on the argument passed in. However, the default is to use the wrapper in .mvn/wrapper/maven-wrapper.jar which is why in projects that have these setup, you only run commands like ./mvnw clean install.

Using Maven Wrapper Config

If you are using Maven Wrapper, then you can specify and configure the following within .mvn directory

  • extensions.xml

  • maven.config

  • jvm.config

This means that all these configurations are applicable per project basis. What's interesting is maven.config where you can pass in just about any maven configuration and it will be "injected" to any command you run.

The structure would be such

root/
├── .mvn/
│   ├── wrapper/
│   │   ├── maven-wrapper.jar
│   │   └── maven-wrapper.properties
│   ├── maven.config
│   ├── settings.xml
│   └── toolchains.xml
└── pom.xml

If you have custom settings per project, define them within settings.xml

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
    <servers>
        <server>
            <id>internal-artifactory</id>
            <username>${env.ARTIFACTORY_USER}</username>
            <password>${env.ARTIFACTORY_PASSWORD}</password>
        </server>
    </servers>
</settings>

And in maven.config, we specify the argument

--settings="./.mvn/settings.xml"

Remember that since Maven 3.9.0, each argument starts in a new line

So when you run any maven command within the project through the wrapper, it will always append --settings="./.mvn/settings.xml" as part of the command

Others

Repositories

Define additional repositories to search for artifacts. The default repository is repo.maven.

What happens is that when you run any maven command, and it looks for dependency, it will first search locally in %USERPROFILE%\.m2 directory. If it cannot find, it will then attempt to locate it remotely via the default repository, or the additional repositories the project defines.

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  ...
  <repositories>
    <repository>
      <releases>
        <enabled>false</enabled>
        <updatePolicy>always</updatePolicy>
        <checksumPolicy>warn</checksumPolicy>
      </releases>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>never</updatePolicy>
        <checksumPolicy>fail</checksumPolicy>
      </snapshots>
      <name>Nexus Snapshots</name>
      <id>snapshots-repo</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
      <layout>default</layout>
    </repository>
  </repositories>
  <pluginRepositories>
    ...
  </pluginRepositories>
  ...
</project>

Sample pom copied from Maven

In most cases, you don't have to specify any, but some organization does have their internal repository hosted using jFrog or Nexus for various reasons.

Distribution Management

This is where we define where to distribute the project artifact during release when it is deployed.

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  ...
  <distributionManagement>
    <repository>
      <uniqueVersion>false</uniqueVersion>
      <id>corp1</id>
      <name>Corporate Repository</name>
      <url>scp://repo/maven2</url>
      <layout>default</layout>
    </repository>
    <snapshotRepository>
      <uniqueVersion>true</uniqueVersion>
      <id>propSnap</id>
      <name>Propellors Snapshots</name>
      <url>sftp://propellers.net/maven</url>
      <layout>legacy</layout>
    </snapshotRepository>
    ...
  </distributionManagement>
  ...
</project>

Sample pom copied from Maven

Settings

In other words, the configuration file for Maven. There are 3 types of settings

  • Global: ${maven.home}/conf/settings.xml

  • User: ${user.home}/.m2/settings.xml

  • Project: ${project.basedir}/.mvn/settings.xml

The first two (Global and User) are the official and default ones. The last (Project) is what can be done if you are using Maven Wrapper (as mentioned above).

Toolchains

The whole idea of using Toolchains are to allow a project to be built independently from the one Maven is relying on.

It is recommended to keep it in .m2 directory but I prefer to place it into .mvn as part of the repository. This way, it removes yet another step for the developer to copy and paste. While I feel that it might be good for the project, but not quite applicable for the general public usage as a whole as every machine configuration pointing to the various JDK paths will be different, and the developer could be working with pointing to a different JDK version in JAVA_HOME

<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
  <!-- JDK toolchains -->
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>17</version>
    </provides>
    <configuration>
      <jdkHome>${env.JAVA_HOME}</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

Profile

Using a profile is a way to pre-define some settings/configuration so it can be activated using -P command

./mvnw clean package -P default

Sometimes, the command can get a little bit long, or it is not used frequently, hence, easy to forget the actual syntax. Using Profile might be a good way to overcome this issue.

<profiles>
    <profile>
        <id>skip-test</id>
        <properties>
            <skipTests>true</skipTests>
        </properties>
    </profile>
</profiles>

The above defined a Profile with skip-test as the id, and denotes that test should be skipped when running this Profile.

So, rather than running the following

./mvnw package -DskipTests

We run the following as a replacement

./mvnw package -P skip-test

Granted, this example doesn't showcase the need to have a Profile, but if you have a more complicated setup, this is where it shines, or if you need to run different settings across different environments.

Conclusion

I hope this is a good enough introduction to Maven and I will follow up with another blog post that covers topics around multi-modules such as the different types of POM (i.e. Aggregate/Parent), using dependencyManagement, pluginManagement.

Personally, my biggest takeaway from this is on Binding plugin goal to Phase where I finally got my "that's how it works" moment as I have always been confused about how it exactly works.

Let me know if any part is confusing!

Source Code

As usual, the full source code is available on GitHub

References