Setup Java Module Project With Maven

Java 9 was major update to java ecosystem. Major piece is from project jigsaw and now we have JPMS short for Java Platform Module System. Historically JDK itself was a large monolith, now it has been broken down to smaller pieces that we call modules. So we can use only required modules in our java application and make custom runtime image. It has only dependencies that application depends on. we can build the runtime image using jlink. In this article I am going to take you through setting up multi module maven project to create a modular java application. I’m going to do it in practical and the way. I used is more sensible and understandable application structure for me. But you can find slight different structure in official quick start.

Setup Java Module Project Maven Module Structure

I am going to implement following module structure. Application is simple repository pattern implementation for data access.

First module is entity, it contains User class and module-info.java. That’s how we mark a module as java module. Let’s peek into the files in entity module.

<?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>

    <parent>
        <groupId>com.aptkode</groupId>
        <artifactId>java9-multi-module</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>entity</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.aptkode</groupId>
            <artifactId>repository</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
package com.aptkode.entity;

import com.aptkode.repository.ID;

import java.util.UUID;

public class User implements ID<User, String> {
    private final String id;
    private final String name;

    public User(String name) {
        this.name = name;
        this.id = UUID.randomUUID().toString();
    }

    public String getName() {
        return name;
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public User getEntity() {
        return this;
    }
}
module com.aptkode.entity {
    requires repository;
    exports com.aptkode.entity;
}

As you can see module-info.java is different kind of java file. It states what it requires and what it exposes. Along the line we’ll see few keywords that we have to use in order to make these modules work together. Let’s look at repository module next. Because it is a dependency for this module.

<?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>

    <parent>
        <groupId>com.aptkode</groupId>
        <artifactId>java9-multi-module</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>repository</artifactId>
</project>
package com.aptkode.repository;

public interface CreateRepository<T,K> {
    T save(ID<T,K> entity);
}
package com.aptkode.repository;

import java.util.List;
import java.util.Optional;

public interface ReadRepository<T, ID> {
    Optional<T> findById(ID id);

    List<T> findAll();
}
package com.aptkode.repository;

public interface ID<T, K> {
    K getId();

    T getEntity();
}
package com.aptkode.repository;

public interface CrudRepository<T,ID> extends ReadRepository<T,ID>, CreateRepository<T,ID> {
}
module repository {
    exports com.aptkode.repository;
}

As you can see dependency happens because of ID interface. Let’s look at the implementation module of this repository, in-memory-repository.

<?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>

    <parent>
        <groupId>com.aptkode</groupId>
        <artifactId>java9-multi-module</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>in-memory-repository</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.aptkode</groupId>
            <artifactId>repository</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.aptkode</groupId>
            <artifactId>entity</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
package com.aptkode.memory.repository;

import com.aptkode.entity.User;
import com.aptkode.repository.CrudRepository;
import com.aptkode.repository.ID;

import java.util.*;

public class InMemoryUserCrudRepo implements CrudRepository<User, String> {

    private final Map<String, User> data = new HashMap<>();

    @Override
    public Optional<User> findById(String id) {
        return Optional.of(data.get(id));
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(data.values());
    }


    @Override
    public User save(ID<User, String> entity) {
        return data.put(entity.getId(), entity.getEntity());
    }
}
import com.aptkode.memory.repository.InMemoryUserCrudRepo;
import com.aptkode.repository.CrudRepository;

module in.memory.repository {
    requires com.aptkode.entity;
    requires repository;
    exports com.aptkode.memory.repository;
    provides CrudRepository with InMemoryUserCrudRepo;
}

We have two new keywords in module-info.java. First one is provides and second one is with. If you had a close look I think now you get what it does, no? have a look again. Basically what it says is, it exposes a class named InMemoryUserCrudRepo which is an implementation of CrudRepository. I’m using IntelliJ Idea for the development, it has nice feature that it locates implementation class as in following screenshot.

All Good, finally let’s have a peek at program module.

<?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>

    <parent>
        <groupId>com.aptkode</groupId>
        <artifactId>java9-multi-module</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>program</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.aptkode</groupId>
            <artifactId>in-memory-repository</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
import com.aptkode.repository.CrudRepository;

module program {
    requires com.aptkode.entity;
    requires repository;
    uses CrudRepository;
    exports com.aptkode;
}

Here ServiceLoader which is a class exists in java since 1.6 now has fresh start. As you can see here we don’t have require statement for in-memory-module, it means that it resolves in runtime. All that happens with the help of ServiceLoader. Now let’s look at how to run this program by building. Following is the root pom of all these modules.

<?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>com.aptkode</groupId>
    <artifactId>java9-multi-module</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>repository</module>
        <module>entity</module>
        <module>in-memory-repository</module>
        <module>program</module>
    </modules>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>13</source>
                        <target>13</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Build with module path

First let’s build repository module, since it doesn’t have any dependencies

// build classes
javac -d target\classes src\main\java\com\aptkode\repository\*.java src\main\java\module-info.java

// build jar
jar --create --file target\repository.jar -C target\classes .

Nothing special, let’s build entity module.

// build classes note --module-path flag
javac -d target\classes --module-path .\..\repository\target\repository.jar src\main\java\com\aptkode\entity\User.java
src\main\java\module-info.java
// build jar
jar --create --file target\entity.jar -C target\classes .

Let’s build in-memory-repository module and program next.

// build classes of in-memory-repository module
javac -d target\classes --module-path .\..\repository\target\repository.jar;.\..\entity\target\entity.jar
 src\main\java\com\aptkode\memory\repository\InMemoryUserCrudRepo.java src\main\java\module-info.java

// build in-memory-repository module jar
jar --create --file target\in-memory-module.jar -C target\classes .

// build classes of program module
javac -d target\classes --module-path .\..\repository\target\repository.jar;.\..\entity\target\entity.jar src\main\jav
a\com\aptkode\Program.java src\main\java\module-info.java

// build program module jar
jar --create --file target\program.jar --main-class com.aptkode.Program -C target\classes .

Now all modules are built. If I go with options of javac one by one -d is for specify where to put class files, –module-path is to locate modules (specified by requires keyword in module-info.java), then list of source files with ; separated on windows and : separated on *nix environments. For jar command –create is for create the archive file, –file to name the jar file and -C where to look for class files and –main-class for specify main program class. you can easily see verbose and much explanatory details of these commands by running them with –help option.

Run with module path

Since we have all the modules ready, lets run it.

java --module-path .\target\program.jar;.\..\repository\target\repository.jar;.\..\entity\target\entity.jar;.\..\in-me
mory-repository\target\in-memory-module.jar -m program/com.aptkode.Program
// output
// [User{id='158aa560-cef7-4024-97a9-158501a6219a', name='tom'}, User{id='f196d7c6-a281-49dc-b648-c6805892fa0d', name='samanta'}, // User{id='6db5d790-7fb1-4060-be2c-6eed73487fa5', name='john'
// }]

This is the hard way of doing it but it teaches us lot of things. Knowing fundamentals is a plus, always.

Build with Maven

Since we are using maven as our build system. we can do the same thing using maven commands. But we have to add maven jar plugin to create jars. And for the program module, we can use the plugin to specify the main class. Let’s look at root pom and program module pom.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.1.2</version>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.1.2</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>com.aptkode.Program</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

Now its very easy to build the application, just run following in the root of directory of the modules. It will create all the required modular jars for the application.

mvn clean package

Run with Maven

In previous section we have built the application. Let’s run it using maven. But as you know maven is a build system and it has no connection to the runtime. So we have to use maven exec plugin. As you already see its easier to setup module path than the classpath we had in pre java 9. But still setting up is bit hard and we have maven to the rescue. Let’s use maven exec plugin to run the application.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.6.0</version>
    <configuration>
        <executable>java</executable>
        <arguments>
            <argument>--module-path</argument>
            <modulepath/>
            <argument>--module</argument>
            <argument>program/com.aptkode.Program</argument>
        </arguments>
    </configuration>
</plugin>

<modulepath/> element takes care of constructing the module path, now go to program module and run following command and you can see the application output.

mvn exec:exec

Wrap it up

We covered how to setup multi module maven project for modular java application. Then we have use ServiceLoader to locate runtime implementation of a service. Finally we used maven to build and run the application also we covered how to do it using java executable programs. The project is available on github. Happy Coding.

Leave a Comment

Your email address will not be published. Required fields are marked *