Managing a Java code project with Maven

We will now move away from basic features of Java towards higher level tools for implementing and maintaining Java programs. We will start by considering a tool for managing the build process and external library dependencies of a Java project. There are several such tools. We have chosen to focus on a tool called Maven, which is probably the most popular Java project build management tool. Maven allows us to e.g. define how thee project should be compiled and what kind of external libraries it needs. Another especially recently quite popular such 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.

Netbeans readily contains Maven, but you may also install Maven separately by e.g. following the instructions provided on 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 issuing the command mvn on 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 subdirectory src. Java source files actually reside in subdirectory src/main/java in a usual subdirectory hierarchy based on packages. E.g. a code file SomeClass.java defined to be in the package com.example would have file path src/main/java/com/example/SomeClass.java.

Like the file suffix suggests, the project file pom.xml describes the project properties in 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 end 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 (in a bit similar manner how packages group code files).

  • 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 and 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. The end also includes a properties element that declares that source files use UTF-8 character encoding, Java source files are compatible with Java version 17, and that the Java compiler will generate class files compatible with 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).

If you create a new Java with Maven/Java Application project in Netbeans, it will automatically create a project file that is quite similar to the example file given above. You can find the project file e.g. as an entry under the “Project files” folder of the project view pane in the left side of the Netbeans main window.

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 listed phases are always executed in succession in such manner that executing a phase entails executing also 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. Java class filess will follow the usual directory hierarchy based on their packages. 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, also it will 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 sigle 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 jar that can package already compiled class files etc. into a JAR file. We will not discuss this program further. 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 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 from all its internal directories and additionally from the JAR file library.jar located in the directory /some/directory. A class path definition can incude also several paths and/or JAR files by separating them with colon (Linux, Mac) or semicolon (Windows) characters. E.g. on Linux the execution java -cp /another/directory/:.:/some/directory/library.jar MyClass would direct the Java virtual machine to search class files from the directory /another/directory, the current working directory ., and the JAR file library.jar in the directory /some/directory.

Fetching external libraries with Maven

One of the leading motives for using Maven is its capability to fetch external class libraries from the internet. Java’s own class libraries are extensive but naturally cannot cover all our potential needs. If there exists an external Java library that offers some functionality we would like to use, it is higly likely that the library can be found from Maven’s central repository that provides access to even millions of libraries. If this is the case, then we can import the library into a maven project by simply adding a corresponding dependency definition into the project file pom.xml. When we then build the project, Maven will automatically download the defined package from the internet and the project can then use its classes and interfaces in a very similar manner as how Java’s own class libraries are used.

A little further below is an example project file that contains a dependency definition: the project file now contains also a dependencies element that in turn contains dependency elements that list the dependencies of the project. The example below defines only one dependency: the jdom2 library that offers tools for processing XML data. A dependency definition specifies the groupId, artifactId and version of the library. These three values allow Maven to identify the exact library and version that should be fetched. This also is the reason why all Maven projects have to specify these three values: if our own project would be published as a library in Maven’s central repository, end users could import our library by specifying the three values groupId, artifactId and version in a dependency definition in their own Maven project files.

Maven central repository

You may wonder how we can know what dependency definition values should be used in order to import a certain library? Quite often the documentation of the library (which we perhaps found via an internet search engine etc.) describes the required Maven dependency definition. In any case we can also search the library from Maven’s central repository, which offers a search page https://search.maven.org/. The central repository e.g. lists different available library versions, and the information for a certain version also describes the corresponding Maven dependency definition.

<?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>
  <dependencies>
    <dependency>
      <groupId>org.jdom</groupId>
      <artifactId>jdom2</artifactId>
      <version>2.0.6.1</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jolira</groupId>
        <artifactId>onejar-maven-plugin</artifactId>
        <version>1.4.4</version>
        <executions>
          <execution>
            <configuration>
              <mainClass>fi.tuni.prog3.example.ExampleMain</mainClass>
              <onejarVersion>0.97</onejarVersion>
              <attachToBuild>true</attachToBuild>
            </configuration>
            <goals>
              <goal>one-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Importing jdom2 as a dependency is not the only new addition in the above example: it now also includes a build element that contains definitions of plugins that we want Maven to use during the build process. The example includes one such plugin element, which sets the project to use a plugin called onejar. This 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 part “one-jar” into the default JAR file name. E.g. if we would use the preceding 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, that is, the class from whose main funtion program execution will start. E.g. the above example defined the project’s main class as fi.tuni.prog3.example.ExampleMain. This definition is required in order to be able to execute the program simply as java -jar program.jar, that is, without a separate parameter that specifies the class from whose main function 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 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 program execution from the class SomeClass that is contained in the JAR file program.jar and belongs to the package com.example.

Posting submission...