Building Java Applications With Maven
Check out this stellar Java tutorial on building applications with Maven.
Join the DZone community and get the full member experience.
Join For FreeAs developers, we spend just as much — if not more — time working on tasks that support our code rather than writing the code itself. This includes writing test cases, creating build and deployment scripts, structuring Continuous Integration (CI) and Continuous Delivery (CD) pipelines, and managing the external dependencies that our projects rely upon. If handled incorrectly, these supports can grind our application development to a halt — regardless of how well our code is written.
Over the years, numerous tools have been developed to reduce the burden on developers, and many have become nearly as indispensable as programming languages themselves. In the Java ecosystem, one of the most popular of these tools is Maven. Maven is a management tool for Java (and Java-based languages) that has become so widely used that tens of millions of binaries from Maven projects have been packaged and hosted for public use.
You may also like: Creating Annotations in Java
In this article, we will delve into the basics of Maven and the concepts behind the tool. In particular, we will explore two of the most important features that Maven provides: (1) Lifecycle management and (2) dependency management. With a foundation in these concepts, we will then look at an example application, walking through the common use cases and scenarios that Java developers will face. The article — far from a comprehensive tutorial — focuses on the pragmatic parts of Maven that we as developers will likely come across and use on a daily basis.
All source code — such as the example application — can be found on GitHub.
The Basics
Maven, at its heart, is a tool that provides a unified build system for Java projects, including utilities for managing the components of the project (such as source code and configuration) in a consistent and straightforward manner. According to the official Maven Introduction:
Maven’s primary goal is to allow a developer to comprehend the complete state of a development effort in the shortest period of time. In order to attain this goal, there are several areas of concern that Maven attempts to deal with:
- Making the build process easy
- Providing a uniform build system
- Providing quality project information
- Providing guidelines for best practices development
- Allowing transparent migration to new features
On a practical level, Maven allows us as Java developers to create a project with a set of defined standards and manage the lifecycle and dependencies of that project. For example, when we need to package our application into a binary — such as a Java Archive (JAR) file — we also need to compile our source code and execute the unit tests. While we could create a series of scripts to do this for us, this approach would not be extensible enough and would be difficult to repeat as we create more projects.
The foundation of this management capability is the structure and configuration of a Maven project, which we will explore in-depth in the remainder of this article. In addition, Maven includes a command-line tool—mvn
— that transforms these concepts into actions and allows us to perform our desired tasks. For example, executing the packaging logic based on the Maven configuration that we provide.
To install the mvn tool, download Maven from the Maven Download page and follow the Maven Installation guide. Once installed, we can test that the mvn
tool works by executing the following on the command line:
mvn --version
This command should result in the following output (versions and operating system may vary):
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00)
Maven home: C:\Users\foo\Documents\Programs\apache-maven-3.5.4\bin\..
Java version: 12.0.2, vendor: Oracle Corporation, runtime: C:\Program Files\Java\jdk-12.0.2
Default locale: en_US, platform encoding: Cp1252
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
Once we have the mvn
tool installed, we can create our first Maven project. Before creating this project, though, we need to understand the concepts behind Maven, starting with the required project structure.
Project Structure
The most evident part of a Maven project is its specific project structure. This starts with a configuration file at the root of the project directory and includes assumptions by Maven about the location of our project artifacts.
Project Object Model
Each Maven project has a set of associated metadata — such as the name, group ID, etc. — that Maven uses to manage the project. This metadata is contained in a Project Object Model (POM) represented by an eXtensible Markup Language (XML) file called pom.xml
. This file must be located at the root directory of the project and must follow a basic structure:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>example</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
</project>
The project
element acts as the root element of our POM, and all other POM elements must be contained within this element. Next, we declare the modelVersion
, which instructs Maven which version of the POM standard we are using — 4.0.0 at the time of writing. Then, we have the groupId
, artifactId
, and version
.
The pair of group ID, artifact ID, and version is called the Group, Artifact, and Version (GAV) of a project and uniquely identifies the project:
- Group: The namespace of the organization that owns the projects
- Artifact: The name of the specific project
- Version: The version of the project
The GAV must follow the Maven naming conventions, but otherwise, there is no restriction on the values provided. For example, we could create a GAV—represented using its standard groupId:artifactId:version
notation—of com.dzone.albanoj2:example-project:1.0.0
. This GAV is essential when dealing with dependencies and managing which version of the project we are working with.
For example, suppose we add a new, backward-incompatible feature to our project, A, and another project, B, depends on A. If we failed to increment the version number of A, recompiling B would include the updated A. Since our change to A is not backward-compatible, the compilation of B would fail. Instead, we should properly manage the version number so that other projects that depend on older versions of our project can be properly preserved.
Lastly, we have the packaging
element, which instructs Maven what the result of our project will be. For example, if we are creating a JAR, we declare this value to be jar
and if we are creating a WAR file, we declare this value to be war
. The complete list of default Maven packaging types can be found in the Maven POM Reference. Note that new packaging types can be created — and thus, we may see other types in actual projects.
Although our POM is basic, it contains all of the information needed by Maven to complete a build. This is possible because of assumptions made by Maven about various configuration elements.
Convention Over Configuration
There are countless configuration elements that would be needed to perform a build, such as the location of the source code, the location resources needed during compilation, and the output location of test files. The sheer volume of this required configuration would become cumbersome and would burden us each and every time we create a new project. Furthermore, we would also clutter our POM with boilerplate elements that — most times — would always contain some default value.
To remedy this problem, Maven takes the approach of using convention over configuration. This technique assumes default values for nearly all of the configuration in our project and allows us to override these defaults when needed.
For example, Maven assumes the following paths for the source code and resources in our project:
src/main/java
: Contains the main Java source codesrc/test/java
: Contains the test Java source codesrc/main/resources
: Contains resources available on the classpathsrc/test/resources
: Contains resources available on the classpath during test execution
The full list of default directories can be found in the official Maven Introduction to the Standard Directory Layout guide. In most scenarios, we will only be concerned with the above four directories, but it is important to understand which files and directories have special meanings.
When necessary, we can change this information to override the default values. For example, the official Using Maven When You Can't Use the Conventions guide provides information on how to change the configured source directory. In most cases, though, the defaults will suffice. In this remainder of this article, we will use the default source and resource directory configuration.
With our basic POM configured, we can look at two of the most important features Maven provides, starting with lifecycle management.
Lifecycle Management
One of the primary objectives of Maven is to manage the lifecycle of a Java project. While building a Java application may appear to be a simple, one-step process, there are actually multiple steps that take place. Maven divides this process into three lifecycles:
clean
: Prepares the project for building by removing unneeded files and dependenciesdefault
: Builds the projectsite
: Creates project documentation
Phases
Maven further subdivides these lifecycles into phases, which represent a stage in the build process. For example, the default
lifecycle includes the following phases (as well as others):
validate
compile
test
package
deploy
In the same way as a deployment pipeline (pp. 103 of Continuous Delivery) granularizes the stages of deployment into discrete steps, Maven also divides its build process into distinct phases. These phases create a chain, where the execution of a later phase executes dependent phases.
For example, if we wish to package an application through a Maven build, our application must first be validated, compiled, and then tested before Maven can generate the resulting package. Thus, when executing the package
phase of a build, Maven with first execute the validate
, compile
, and test
phases of the build before finally executing the package
phase. Maven phases, therefore, act as a sequence of ordered steps.
We can execute phases by supplying them as command-line arguments to the mvn
command:
mvn package
Goals & Plugins
Maven breaks phases down one more time into goals, which represent discrete tasks that are executed as part of each phase. For example, when we execute the compile
phase in a Maven build, we are actually compiling both the main sources that make up our project as well as the test sources that will be used when executing our automated test cases.
Thus, the compile
phase is composed of two goals:
compiler:compile
compiler:testCompile
The compiler
portion of the goal is the plugin name. A Maven plugin is an artifact that supplies Maven goals. The addition of these plugins allows Maven to be extended beyond its basic functionality.
For example, suppose that we wish to add a goal that verifies that our code meets the formatting standard of our company. To do this, we could create a new plugin that has a goal that checks the source code and compares it to our company standard, succeeding if our code meets the standard and failing otherwise.
We can then tie this goal into the validate
phase so that when Maven runs the validate
phase (such as when the compile
phase is run), our custom goal is executed. Creating such a plugin is outside the scope of this article, but detailed information can be found in the official Maven Plugin Development documentation.
Note that a goal may be associated with zero or more phases. If no phase is associated with the goal, the goal will not be included in a build by default but can be explicitly executed. For example, if we create a goal foo:bar
that is not associated with any phase, Maven will not execute this goal for us (since no dependency is created to a phase that Maven is executing), but we can explicitly instruct Maven to execute this goal on the command line:
mvn foo:bar
Likewise, a phase can have zero or more goals associated with it. If a phase does not have any goals associated with it, though, it will not be executed by Maven.
For a complete list of all phases and goals included in Maven by default, see the official Maven Introduction to the Build Lifecycle documentation.
Dependency Management
The second core feature of Maven is dependency management. A dependency is an artifact that a software application requires — either at compile-time or at run-time. For simple applications, we can manually manage dependencies, such as by adding a JAR to the classpath during compilation. As the scale of an application grows, though, manual management becomes unruly.
Not only are we responsible for managing the direct dependencies that our project has, but we are also responsible for handling the transitive dependencies — i.e., the dependencies of our dependencies. Adding a single dependency to our project can mean that we have to manage ten more dependencies that our dependency requires, and possibly dozens more that each of these transitive dependencies in-turn require.
Declaring Dependencies
Instead of managing this dependency tree manually, we can use Maven to automatically resolve and download our dependencies for us, ensuring that they are compiled and linked correctly so that our application can execute at run-time. To do this, we must declare our dependencies in our POM under the dependencies
element:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>example</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Each dependency is declared using a new dependency
element, which contains the GAV of the dependency, along with supplemental information. Note that the GAV allows Maven to uniquely identify the dependency. Thus, all of our dependencies must be Maven projects themselves. We will see below how Maven resolves the dependency GAV into a JAR — or similar binary — but in general, the GAV we provide in our dependency declaration is used to find a binary with a POM that contains a matching GAV.
The most common supplemental element is the scope
, which defines when the dependency should be used (i.e., included on the classpath). There are five common scopes that are used:
- compile: The dependency will be present on all classpaths and propagated to dependent projects
- provided: Same as compile, but the dependency is expected to be provided at runtime by the execution environment — such as the Java Development Kit (JDK) or a web container
- runtime: The dependency is not required at compile-time, but is required at run-time
- test: The dependency is only required while compiling and executing tests and will not be present on the classpath during normal execution or propagated to dependent projects
- system: Same as compile, but the path of the dependency must be explicitly provided using the
systemPath
element (see the System Dependencies documentation for more information)
By default, the compile scope is used if no scope is explicitly provided. In the case above, we have added a test
dependency, which will only be included in the classpath during the testing phase. For a complete list of scopes, see the Maven Introduction to the Dependency Mechanism guide.
Apart from scoping information, we can also exclude transitive dependencies if needed. For example, if one of our dependencies pulls in a conflicting version of a binary, we can exclude this transitive dependency by adding an exclusion element — containing a group and artifact ID — under the exclusions
element:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>example</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>some.group.id</groupId>
<artifactId>some.artifact.id</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Resolving Dependencies From Repositories
With our dependencies declared, Maven can now resolve each dependency into a suitable binary and using this binary to compile and link the necessary bytecode to our application. To do this, Maven looks at a repository or set of repositories for an appropriate binary and downloads the binary. A repository is simply a server that stores compiled Maven projects, along with their respective GAVs, which allows Maven to find a binary associated with a specific GAV.
Some common Maven repositories include:
- Default Maven repository (used if none is explicitly provided)
- Maven Central
- Maven Repository
These repositories allow Maven to download the desired dependency and use it during compilation or linking, eventually building a compiled executable from our project.
If no binary maps to a required dependency, Maven will inform us during the build:
[WARNING] The POM for some.dep.group:some.dep.artifact:jar:1.0.0 is missing, no dependency information available
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.753 s
[INFO] Finished at: 2019-11-25T10:10:47-05:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project adder: Could not resolve dependencies for project com.example:example:jar:1.0.0: Failure to find some.dep.group:some.dep.artifact:jar:1.0.0 in https://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forced -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException
While the dependency resolution process can be complicated under the hood, it suffices for us to understand that Maven can take the dependency information we declare in our POM and resolve this into a binary using a repository. For more information — and to gain a more nuanced understanding of how Maven handles dependencies — see the official Maven Introduction to Repositories guide.
Example Application
With an understanding of the theory and techniques that Maven uses to manage a project, we can now develop our own Maven project. In this application, we will create a simple adder that computes the sum of two integers. During this process, we will:
- Generate a Maven project using the Maven command-line tool
- Configure the
pom.xml
file - Create main and test source code files
- Execute a Maven build
- Execute the resulting JAR file
Generating a Project
The first step to create our application is to generate the basic project structure. While we could do this manually, Maven includes a goal that will generate a quickstart project for us.
To generate this project, we execute the following command:
mvn archetype:generate \
-DgroupId=com.dzone.albanoj2.maven.java \
-DartifactId=adder \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=false
Maven will then generate the following project structure for us:
adder/
|- src/
| |- main
| | +- java/
| | +- com/dzone/albanoj2/maven/java
| | +- App.java
| +- test
| +- java/
| +- com/dzone/albanoj2/maven/java
| +- AppTest.java
+- pom.xml
Configuring the POM
Although the pom.xml
file has been generated for us, it contains boilerplate configuration that is unnecessary for our purposes. Instead, we will replace the entire contents of the pom.xml
file with the following configuration:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dzone.albanoj2.maven.java</groupId>
<artifactId>adder</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<junit.version>5.5.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.dzone.albanoj2.maven.java.Application</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Starting from the top, we have our standard model version and GAV. After the GAV, we have our properties. These properties are key-value pairs, where the element name is the key, and the element value is the value. For example, the entry <maven.compiler.target>11</maven.compiler.target>
creates a property where maven.compiler.target
is the key and 11
is the value.
Generally, properties can be any arbitrarily-named entry — so long as the key is a valid XML element name— and can be used elsewhere in the POM using the ${<key>}
syntax. For example, we declare a property <junit.version>5.5.2</junit.version>
and use it in the <version>
element of our dependency: <version>${junit.version}</version>
.
Other properties, such as maven.compiler.source
, have special meaning. We have four of these special-case properties in our POM:
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
The first two define the encoding type of our source code and reporting output, respectively. If we omit these encoding types, Maven will warn us during compilation that our build is platform-dependent:
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
The last two properties configure the JDK version of our source code and the expected target JDK version, respectively. These keys are known by the Java compiler plugin for Maven and will, in turn, set the source and target JDK versions of the compiler.
Next, we declare our JUnit dependency (under the dependencies
element), which allows us to execute our automated tests. Note that the scope is test
, which ensures that this dependency is not compiled or resolved in our executable application — only during the test phase.
Lastly, we add the build
element to override the default build configuration. In this case, we override the configuration for the maven-jar-plugin
plugin to set the fully-qualified name of the main class that will be executed when we run our generated JAR file. While this is a concise override, there is an array of other options that can be overridden if needed. For more information, see the official Maven JAR Plugin documentation.
Creating the Main & Test Source Code
With our project structure and POM established, we can now add the source code to our project. The first step is to delete the App.java
and AppTest.java
files in src/main/java/com/dzone/albanoj2/maven/java
and src/test/java/com/dzone/albanoj2/maven/java
, respectively. We will then create a new file called Adder.java
in src/main/java/com/dzone/albanoj2/maven/java
:
package com.dzone.albanoj2.maven.java;
public class Adder {
public int add(int a, int b) {
return a + b;
}
}
Next, we create an Application
class in the same directory that includes a basic main
method:
package com.dzone.albanoj2.maven.java;
public class Application {
public static void main(String[] args) {
Adder adder = new Adder();
System.out.println("2 + 2 = " + adder.add(2, 2));
}
}
Lastly, we will create a new file called AdderTest.java
in src/test/java/com/dzone/albanoj2/maven/java
that will act as our unit test for our Adder
class:
package com.dzone.albanoj2.maven.java;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.dzone.albanoj2.maven.java.Adder;
public class AdderTest {
private Adder adder;
@BeforeEach
public void setUp() {
adder = new Adder();
}
@Test
public void whenAddTwoZeros_ThenSumIsZero() {
assertEquals(0, adder.add(0, 0));
}
@Test
public void whenAddFirstZeroSecondNegative_ThenSumIsEqualToSecond() {
assertEquals(-1, adder.add(0, -1));
}
@Test
public void whenAddFirstNegativeSecondZero_ThenSumIsEqualToFirst() {
assertEquals(-1, adder.add(-1, 0));
}
@Test
public void whenTwoNegatives_ThenSumIsCorrect() {
assertEquals(-3, adder.add(-1, -2));
}
@Test
public void whenAddFirstZeroSecondPositive_ThenSumIsEqualToSecond() {
assertEquals(1, adder.add(0, 1));
}
@Test
public void whenAddFirstPositiveSecondZero_ThenSumIsEqualToFirst() {
assertEquals(1, adder.add(1, 0));
}
@Test
public void whenTwoPositives_ThenSumIsCorrect() {
assertEquals(3, adder.add(1, 2));
}
@Test
public void whenAddFirstPositiveSecondNegative_ThenSumIsCorrect() {
assertEquals(0, adder.add(1, -1));
}
@Test
public void whenAddFirstNegativeSecondPositive_ThenSumIsCorrect() {
assertEquals(0, adder.add(-1, 1));
}
}
This results in the following directory structure:
adder/
|- src/
| |- main
| | +- java/
| | +- com/dzone/albanoj2/maven/java
| | |- Adder.java
| | +- Application.java
| +- test
| +- java/
| +- com/dzone/albanoj2/maven/java
| +- AdderTest.java
+- pom.xml
Executing the Build
We now have all of the components necessary to execute our Maven build. Our goal for this build will be to generate a JAR file that we can execute from the command-line. Therefore, we will use the Maven package
goal to generate our JAR.
Before starting the build, we must change the directory to our project root (the adder/
directory). Once inside the project root, we then execute the following command:
mvn package
This will start our build, which will result in the following output:
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------< com.dzone.albanoj2.maven.java:adder >-----------------
[INFO] Building adder 1.0.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ adder ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ adder ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ adder ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ adder ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ adder ---
[INFO] Surefire report directory: C:\Users\foo\Documents\GitHub\dzone-java-maven-adder\target\surefire-reports
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.dzone.albanoj2.maven.java.AdderTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec
Results :
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ adder ---
[INFO] Building jar: C:\Users\foo\Documents\GitHub\dzone-java-maven-adder\target\adder-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.835 s
[INFO] Finished at: 2019-11-22T09:17:08-05:00
[INFO] ------------------------------------------------------------------------
Note that the output may contain some downloading message, such as:
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/surefire/surefire-junit3/2.12.4/surefire-junit3-2.12.4.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/surefire/surefire-junit3/2.12.4/surefire-junit3-2.12.4.pom (1.7 kB at 4.7 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/surefire/surefire-junit3/2.12.4/surefire-junit3-2.12.4.jar
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/surefire/surefire-junit3/2.12.4/surefire-junit3-2.12.4.jar (26 kB at 291 kB/s)
These messages are expected and result from Maven downloading our referenced dependencies. The important portion of the output to look for in the footer, which states that the execution of the package
goal successfully completed:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.835 s
[INFO] Finished at: 2019-11-22T09:17:08-05:00
[INFO] ------------------------------------------------------------------------
Also, note that our JUnit tests were executed as part of our build:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.dzone.albanoj2.maven.java.AdderTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec
Results :
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
This is important for two reasons: (1) we know that the package
goal executed the dependent test
, goal, and (2) we know that our unit tests will need to pass before a JAR file is generated by our build. If one of the tests were to fail, it would be reported, and the build would fail. This ensures that Maven does not generate a new JAR file for code that does not pass its tests.
Finding & Running Packaged JAR
Once the build completes, we can find the packaged JAR — adder-1.0.0.jar
— in the target/
directory. To execute this JAR, we change directory to target/
and execute the following command:
java -jar adder-1.0.0.jar
This execution results in the following output:
2 + 2 = 4
While this may seem trivial, we have successfully created an application that can be compiled, tested, and packaged using a single mvn
command and subsequently executed using the java
command.
This basic project can be expanded if needed and integrated with a CI or CD pipeline. Doing so will allow our application to be checked into a source code repository (such as Git) and automatically built by a tool such as Jenkins. For more information, see the Jenkins Build a Java App With Maven guide.
Conclusion
Maven has become a ubiquitous part of countless Java projects and has profoundly changed the way that Java development is performed. In this article, we looked at the basics of Maven and delved into its fundamental concepts — including lifecycle and dependency management. We also walked through a complete example of a Maven project, demonstrating how Maven is commonly used in real-world development.
While Maven is a complex and powerful tool, understanding its basic features and the thought process behind its operation can go a long way in improving development and reducing the burden of managing the non-coding aspects of Java development.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments