Managing a Java code project with Maven

In this course we will use a tool called Apache Maven for implementing and maintaining Java programs. There are other similar tools to achieve the same goal, but Maven is probably the most popular one. Maven allows us to, e.g. define how the project should be compiled and what kind of external libraries it needs. Another similar, especially recently quite popular tool is Gradle. Maven plays a similar role in a Java project as, e.g. the npm package management tool in Node.js-based JavaScript projects. We will only provide a small glimpse into Maven; just enough to fulfill the needs of the course. Maven will be used in all the coding exercises in this course.

NetBeans readily comes with Maven, but you may also install Maven separately by, e.g. following the instructions provided on the Maven homepage or in Linux by using the system’s package manager. If Maven has been installed properly, you should be able to run it by entering the command mvn in a command line prompt.

Maven project structure

A Maven project consists of mainly two things: a project configuration file named pom.xml, which is stored in the project’s root directory, and source files, which are stored under the subdirectory src. Java source files reside in the subdirectory src/main/java. E.g. a code file SomeClass.java would have the file path src/main/java/SomeClass.java.

Like the file extension suggests, the project file pom.xml describes the project properties using the XML format. If you are not yet familiar with XML, it is a good idea to learn its basic features, e.g. by reading the Wikipedia article.

Below is an example of an almost minimal Maven project file:

<?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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tuni.prog3</groupId>
  <artifactId>example</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
</project>

pom.xml must contain at least the following parts:

  • A project root element (begins with a <project> tag and ends with a </project> tag). All elements listed below are inside the root element.

  • A modelVersion element whose value should at present always be 4.0.0.

    • This tells which Maven standard (or “model”) the project file corresponds to. The current standard is 4.0.0.

  • A groupId element that tells the group of the project.

    • Maven uses this information to group projects in a hierarchical manner.

  • An artifactId element that tells the name of the project.

  • A version element that tells the version of the project.

    • This would usually be incremented as the program is developed further. E.g. version 1.0 could be followed by version 1.1 or version 2.0, and so on.

The preceding project file also included a packaging element that defines what type of a package Maven should generate; the value jar specifies that the program’s compiled class files and possible other resources will be packed into a so called JAR file (this would also be Maven’s default behaviour). This is discussed further below. Towards the end there’s also a properties element that declares that source files use the UTF-8 character encoding, that Java source files are compatible with the Java version 17, and that the Java compiler will generate class files compatible with the Java version 17. You should use Java version settings that are compatible with your installed Java environment (the project cannot use a higher version than what the environment supports; lower versions can be used).

NetBeans

A Maven project is created in NetBeans by either navigating to File | New Project... or clicking the folder icon with a small plus sign, followed by Java with Maven and Java Application. NetBeans prompts you for the information to create the project and then creates a project file roughly matching the example file given above, the src/main/java directory and a class containing the main method, which you can delete if you so choose. The contents of the project can be explored in the Projects tab in the upper-left corner. The project file (pom.xml) can be found under Project files, and the source code files under Source Packages.

A project can be created without specifying a package (emptying the Package: field). In this case NetBeans won’t attached the source code files to any package and places the source code files under Project Packages | <default packages>. This is desirable with the exercises of the first couple of weeks which don’t specify a package where the code should be placed.

NetBeans automatically appends the string -SNAPSHOT to the version number which signifies that the project is in development (unstable). In this course you can (and should) remove this string, as the projects are small enough to give them the final version number from the start. E.g. version 1.0-SNAPSHOT becomes 1.0. From now on it is assumed that version numbers are given without the -SNAPSHOT suffix.

Buildind (e.g. compiling) a Maven project

You may build a maven project either by using an IDE that supports Maven or manually on the command line. Some IDE’s (e.g. NetBeans) have built-in Maven support, but others (e.g. VS Code) require installing a separate plugin.

Maven’s build process consists of multiple phases. Some of the main phases are:

  • compile: Compiles all source files under the project’s src subdirectory.

  • test: Performs automated tests with the compiled program (if such tests have been defined).

  • package: Packages the program into a distributable format, such as a JAR file.

  • install: Stores the package into Maven’s local repository.

    • E.g., in Linux Maven might store a package in the directory .m2/repository under the user’s home directory. Maven by default prints out information about the name and location of the stored package.

This was only a partial list of Maven’s build phases.

A maven project can be built on the command line by issuing a command of form mvn phase, where phase specifies the phase we want to execute. The phases listed above are always executed in succession so that executing a phase also implies executing all earlier phases. E.g. mvn compile compiles the project, mvn test first compiles and then tests, and mvn package first compiles, then tests and finally packages it.

Maven by default places the compiled class files into the subdirectory target/classes. If a project is packaged, e.g. into a JAR file, Maven will place the package into the root of the target subdirectory. The name of the created package is by default derived from the information defined in the project file: it is typically named in the form artifactId-version.jar.

The Run/Build project -command offered by NetBeans by default corresponds to issuing the command mvn install. Since install is a later phase than package, it will also package the program into the target subdirectory.

JAR files (Java Archive files)

Java compiler tools support packaging the files of a program (or a class library) into a single file (a JAR file) that can then be conveniently distributed to end users. A JAR file contains all compiled class files, and possibly also some other resources, of the program, and is actually just a ZIP file that has been renamed to use the file suffix “.jar”. Therefore it is, e.g. possible to use general ZIP tools to inspect, extract or modify the contents of a JAR file.

The Java compiler suite contains a command line program called jar that can package already compiled class files, etc., into a JAR file. We will not discuss this program further, as using Maven will suffice in this course.

A program that has been packaged into a JAR file can be executed by giving the switch -jar and the JAR file name to the Java Virtual Machine. For example java -jar program.jar would execute a program that has been packaged into the JAR file program.jar.

Java class libraries are practically always distributed as JAR files. If we want to execute a program that relies on some classes or interfaces provided by some JAR file library.jar, Java needs to be able to find the library file. The file must be either placed into a directory that Java would anyway inspect (its own internal class directories and the current working directory) or we must separately specify with a “-cp” switch a class path that leads to the file. For example java -cp /some/directory/library.jar MyClass would execute the class MyClass in such manner that Java searches class files required during program execution in all its internal directories and additionally in the JAR file library.jar located in the directory /some/directory. A class path definition can include several paths and/or JAR files by separating them with a colon (Linux, Mac) or a semicolon (Windows) character. E.g. on Linux the execution java -cp /another/directory/:.:/some/directory/library.jar MyClass would direct the Java virtual machine to search for class files in the directory /another/directory, the current working directory ., and the JAR file library.jar in the directory /some/directory.

Using plugins with Maven

A Maven project file typically also includes a build-element that contains definitions of plugins that we want Maven to use during the build process. One such plugin that will be frequently used in this course is a plugin called onejar. Here’s an example project file for including the onejar plugin in a project:

<?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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tuni.prog3</groupId>
  <artifactId>example</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jolira</groupId>
        <artifactId>onejar-maven-plugin</artifactId>
        <version>1.4.4</version>
        <executions>
          <execution>
            <configuration>
              <mainClass>ExampleMain</mainClass>
              <onejarVersion>0.97</onejarVersion>
              <attachToBuild>true</attachToBuild>
            </configuration>
            <goals>
              <goal>one-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

The onejar plugin offers a convenient feature that packages also external libraries into the JAR file. Maven would otherwise package only the project’s own class files, etc., into the JAR file, and then an end user would need to have both the JAR file of the project and the JAR files of any required external libraries in order to be able to execute the program. When we use onejar, the packaged JAR file will be self-sufficient. There will actually be two JAR files: one is the default one without external libraries and the other one contains also external libraries. The latter is by default named by adding the string “one-jar” into the default JAR file name. E.g. if we would use the above project file, the command mvn package would create the JAR files target/example-1.0.jar and target/example-1.0.one-jar.jar.

A onejar plugin definition contains one value that we should usually define explicitly for each different project: the mainClass element. It defines the main class of the project, i.e. the class from whose main funtion the program execution will start. E.g. the above example defined the project’s main class as ExampleMain. This definition is required in order to be able to execute the program simply as java -jar program.jar, i.e. without a separate parameter that specifies the class from whose main function the program execution should start. But note that a project does not have to include a main function at all if it is intended to be used as a class library.

If you use the onejar plugin without defining a main class, or if you want to start the program execution from some other class of the project, it is possible to provide the main class with a command line switch of form -Done-jar.main.class=MainClass, where MainClass is the main class (including the package prefix, if the class has any). For example java -Done-jar.main.class=com.example.SomeClass -jar program.jar would start the program execution from the class SomeClass which is contained in the JAR file program.jar and belongs to the package com.example.