The Java Module System in practice

About me

  • Software engineer
  • 15+ years of experience in JVM based languages
  • Open-Source contributor
scoop logo
Serban Iordache

  Serban Iordache

Java SE Modules (Java 9)

modules java9

Java SE Modules (Java 9)

modules java9 deprecated

Java SE Modules (Java 11 and 12)

modules java11

Dealing with removed modules

Product.java (scroll to see more code)
package org.example.jpms;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Product {
    private int id;
    private String name;
    private double price; // Using double for money is actually a bad idea

    public Product() {
        this(-1, "", 0);
    }

    public Product(int id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return name + ": " + price;
    }
}

Dealing with removed modules

XMLPrinter.java
package org.example.jpms;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class XMLPrinter {
    public static void main(String[] args) throws Exception {
        Product product = new Product(100, "pizza", 3.25);

        JAXBContext jaxbContext = JAXBContext.newInstance(Product.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        jaxbMarshaller.marshal(product, System.out);
    }
}

Dealing with removed modules

ProductTest.java
package org.example.jpms;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ProductTest {
    @Test
    public void testToString() {
        Product p = new Product(33, "spaghetti", 2.15);
        Assertions.assertEquals("spaghetti: 2.15", p.toString());
    }
}

Dealing with removed modules

build.gradle
plugins {
    id 'application'
}
repositories {
    jcenter()
}

mainClassName = 'org.example.jpms.XMLPrinter'

dependencies {

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}
test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

Replacements for removed modules

Module name Package Maven artifact

java.activation

javax.activation.*

com.sun.activation:javax.activation:1.2.0

java.corba

javax.activity.*
javax.rmi.*
org.omg.*

org.jboss.openjdk-orb:openjdk-orb:8.1.2.Final

java.transaction

javax.transaction.*

javax.transaction:javax.transaction-api:1.3

java.xml.bind

javax.xml.bind.*

org.glassfish.jaxb:jaxb-runtime:2.3.2

java.xml.ws

javax.xml.ws.*
javax.jws.*
javax.soap.*

com.sun.xml.ws:jaxws-ri:2.3.2

java.xml.ws.annotation

javax.annotation.*

javax.annotation:javax.annotation-api:1.3.2

Dealing with removed modules

build.gradle
plugins {
    id 'application'
}
repositories {
    jcenter()
}

mainClassName = 'org.example.jpms.XMLPrinter'

dependencies {

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}
test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

Dealing with removed modules

build.gradle
plugins {
    id 'application'
}
repositories {
    jcenter()
}

mainClassName = 'org.example.jpms.XMLPrinter'

dependencies {
    implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.2'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}
test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

Module descriptors

module info

Module descriptors

module-info.java
module org.example.mylib {
    requires java.desktop;
    requires transitive java.sql;






}

Module descriptors

module-info.java
module org.example.mylib {
    requires java.desktop;
    requires transitive java.sql;
    exports org.example.mylib.core;
    exports org.example.mylib.impl to org.example.gui;




}

Module descriptors

module-info.java
module org.example.mylib {
    requires java.desktop;
    requires transitive java.sql;
    exports org.example.mylib.core;
    exports org.example.mylib.impl to org.example.gui;
    opens org.example.mylib.model;
    opens org.example.mylib.impl to java.xml.bind;


}

Module descriptors

module-info.java
module org.example.mylib {
    requires java.desktop;
    requires transitive java.sql;
    exports org.example.mylib.core;
    exports org.example.mylib.impl to org.example.gui;
    opens org.example.mylib.model;
    opens org.example.mylib.impl to java.xml.bind;
    uses javax.annotation.processing.Processor;
    provides org.example.mylib.Solver with org.example.mylib.impl.MySolver;
}

VM arguments to break encapsulation

Options for the javac and java tools:

  • --add-exports module/package=other-module(,other-module)*
    Specifies a package to be considered as exported from its defining module to additional modules.
     

     
  • --add-reads module=other-module(,other-module)*
    Specifies additional modules to be considered as required by a given module.
     

     
  • --add-opens module/package=target-module(,target-module)*
    Updates module to open package to target-module, regardless of module declaration.
    (has no effect on javac)

Classpath and module path

Specifying the classpath and the module-path with the javac and java tools:

classpath module-path

--class-path <path>
-classpath <path>
-cp <path>

--module-path <path>
-p <path>

Classpath and module path

You can mix --class-path and --module-path.

  • modular JAR on the module-path
  • modular JAR on the classpath
  • non-modular JAR on the module-path
  • non-modular JAR on the classpath

 
All code on the classpath is part of the unnamed module, which:

  • exports all code on the classpath
  • reads all other modules
  • it is readable only from automatic modules!

Automatic modules

Non-modular JARs found on the module-path are turned into automatic modules.

  • A module descriptor is generated on the fly

    • it requires transitive all other resolved modules
    • it exports all its packages
    • it reads the unnamed module

Split packages

Packages are not allowed to span different modules.

 
Dealing with split packages using the javac and java tools:

--patch-module module=file(:file)*
    Merges all types from a list of artifacts into the given module.

 

Example:
java
  --module-path ...
  --add-modules ...
  --patch-module java.xml.ws.annotation=/path/to/my/lib/jsr305-3.0.2.jar
  --module org.example.hello/org.example.hello.HelloWorld

Building a modular application with Gradle

module-info.java
module org.example.jpms {
    requires java.xml.bind;
    opens org.example.jpms to java.xml.bind;
}

Building a modular application with Gradle

XMLPrinter.java
package org.example.jpms;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class XMLPrinter {
    public static void main(String[] args) throws Exception {
        Product product = new Product(100, "pizza", 3.25);
        JAXBContext jaxbContext = JAXBContext.newInstance(Product.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        jaxbMarshaller.marshal(product, System.out);
        System.out.printf("\n\nRunning module: %s\n",
                            XMLPrinter.class.getModule().getName());
    }
}

Building a modular application with Gradle

ProductTest.java
package org.example.jpms;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ProductTest {
    @Test
    public void testToString() {
        Product p = new Product(33, "spaghetti", 2.15);
        Assertions.assertEquals("spaghetti: 2.15", p.toString());
        System.out.printf("\n\nTesting module: %s\n",
                            ProductTest.class.getModule().getName());
    }
}

Building a modular application with Gradle

To allow compilation:
compileJava {
    doFirst {
        options.compilerArgs = ['--module-path', classpath.asPath]
        classpath = files()
    }
}

To run on the module-path:
run {
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', moduleName
        ]
        classpath = files()
    }
}

Building a modular application with Gradle

To compile the unit tests:
compileTestJava {
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'org.junit.jupiter.api',
                '--add-reads', "$moduleName=org.junit.jupiter.api",
                '--patch-module',
                "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
        ]
        classpath = files()
    }
}

Building a modular application with Gradle

To run the unit tests on the module-path:
test {
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'ALL-MODULE-PATH',
                '--add-reads', "$moduleName=org.junit.jupiter.api",
                '--patch-module',
                "$moduleName=" + files(sourceSets.test.java.outputDir).asPath,
                '--add-opens',
                "$moduleName/org.example.jpms=org.junit.platform.commons"
        ]
        classpath = files()
    }
}

Building a modular application with Gradle

To adjust the start scripts:
import java.util.regex.Matcher
startScripts {
    doFirst {
        defaultJvmOpts = [
                '--module-path', 'LIB_DIR_PLACEHOLDER',
                '--add-modules', moduleName,
        ]
    }
    doLast{
        def bashFile = new File(outputDir, applicationName)
        bashFile.text = bashFile.text.replaceFirst('LIB_DIR_PLACEHOLDER',
                                    Matcher.quoteReplacement('$APP_HOME/lib'))
        def batFile = new File(outputDir, applicationName + ".bat")
        batFile.text = batFile.text.replaceFirst('LIB_DIR_PLACEHOLDER',
                                    Matcher.quoteReplacement('%APP_HOME%\\lib'))
    }
}

Using Gradle with the moduleplugin

plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.5.0"
}
repositories {
    jcenter()
}

mainClassName = 'org.example.jpms/org.example.jpms.XMLPrinter'

dependencies {
    implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.2'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}
test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

Modular JARs compatible with Java 8 (or earlier)

1. compile all source files except module-info.java

  • specify a value ≤ 8 for the --release option

2. compile module-info.java

  • specify a value ≥ 9 for the --release option
  • include the location of the class files produced in step 1 in the value passed to the --module-path option

3. create a JAR containing the class files produced in steps 1 and 2.

 
Maven: configure the pom.xml as shown in the documentation.
Gradle: use the org.javamodularity.moduleplugin plugin.

Modular JARs compatible with Java 8 (or earlier)

build.gradle
plugins {
    id 'java'
    id "org.javamodularity.moduleplugin" version "1.5.0"
}
modularity.mixedJavaRelease 8
repositories {
    jcenter()
}
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}
test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

Custom runtime images for modular applications

jlink

assembles and optimizes a set of modules and their dependencies into a custom runtime image

  • a custom runtime image is a special distribution containing the bare minimum to run an application.
  • you can create your own JRE.
  • you can use jlink only if all artifacts in the dependency graph are modularized!

Custom runtime images for modular applications

HelloWorldVerticle.java
public class HelloWorldVerticle extends AbstractVerticle {
    @Override
    public void start(Future<Void> future) {
        vertx.createHttpServer()
            .requestHandler(r -> {
                String name = Optional.ofNullable( r.getParam( "name" ))
                                          .orElse( "nameless stranger" );
                r.response().end ( "Hello, " + name + "!" );
            })
            .listen( 8080, result -> {
                if ( result.succeeded() ) { future.complete(); }
                else { future.fail(result.cause()); }
            } );
    }
}

Custom runtime images for modular applications

HelloWorldServer.java
package com.example;

import io.vertx.core.Vertx;

public class HelloWorldServer {
    public static void main(final String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle( HelloWorldVerticle.class.getName() );
    }
}

module-info.java
module com.example {
    requires vertx.core;
    exports com.example;
}

Custom runtime images for modular applications

build.gradle
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.5.0"

}
repositories { jcenter() }
dependencies {
    implementation "io.vertx:vertx-core:3.5.0"
}
mainClassName = "com.example/com.example.HelloWorldServer"
run.jvmArgs = ["--add-modules", "jdk.unsupported",
                        "--add-reads", "netty.common=jdk.unsupported"]

Custom runtime images for modular applications

The ModiTect approach:

  • for each non-modularized artifact in the dependency graph:

    • generate a module descriptor and add it to the artifact

 
ModiTect  can be used with both Maven and Gradle.

Custom runtime images for modular applications

pom.xml for the ModiTect approach (scroll to see more code)
<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>org.moditect</groupId>
    <artifactId>moditect-integrationtest-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>

  <artifactId>moditect-integrationtest-vertx</artifactId>
  <packaging>jar</packaging>
  <name>ModiTect Integration Test - Vert.x</name>

  <properties>
    <io.vertx.version>3.5.0</io.vertx.version>
    <io.netty.version>4.1.15.Final</io.netty.version>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-dependencies</artifactId>
        <version>${io.vertx.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-core</artifactId>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
            <release>9</release>
            <compilerArgs>
              <compilerArg>--module-path</compilerArg>
              <compilerArg>${project.build.directory}/modules</compilerArg>
            </compilerArgs>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>

    <plugins>
      <plugin>
        <groupId>${project.groupId}</groupId>
        <artifactId>moditect-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>add-module-info-to-dependencies</id>
            <phase>package</phase>
            <configuration>
              <outputDirectory>
                ${project.build.directory}/modules
              </outputDirectory>
              <modules>
                <module>
                  <artifact>
                    <groupId>com.fasterxml.jackson.core</groupId>
                      <artifactId>jackson-core</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>com.fasterxml.jackson.core</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>com.fasterxml.jackson.core</groupId>
                      <artifactId>jackson-annotations</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>com.fasterxml.jackson.annotations</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-databind</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>com.fasterxml.jackson.databind</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-common</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.common</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-buffer</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.buffer</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-codec</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.codec</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-resolver</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.resolver</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.transport</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-codec-dns</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.codec.dns</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-codec-http2</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.codec.http2</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-resolver-dns</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.resolver.dns</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport-native-unix-common</artifactId>
                    <version>${io.netty.version}</version>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.channel.unix</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport-native-epoll</artifactId>
                  </artifact>
                  <moduleInfoSource>
                    module io.netty.channel.epoll {
                      requires io.netty.buffer;
                      requires io.netty.channel.unix;
                      requires io.netty.common;
                      requires io.netty.transport;
                      exports io.netty.channel.epoll;
                    }
                  </moduleInfoSource>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport-native-kqueue</artifactId>
                  </artifact>
                  <moduleInfoSource>
                    module io.netty.channel.kqueue {
                      exports io.netty.channel.kqueue;
                    }
                  </moduleInfoSource>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-handler</artifactId>
                  </artifact>
                  <moduleInfoSource>
                    module io.netty.handler {
                      exports io.netty.handler.flow;
                      exports io.netty.handler.flush;
                      exports io.netty.handler.ipfilter;
                      exports io.netty.handler.logging;
                      exports io.netty.handler.ssl;
                      exports io.netty.handler.ssl.ocsp;
                      exports io.netty.handler.ssl.util;
                      exports io.netty.handler.stream;
                      exports io.netty.handler.timeout;
                      exports io.netty.handler.traffic;
                    }
                  </moduleInfoSource>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-codec-socks</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.codec.socks</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-handler-proxy</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.handler.proxy</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-codec-http</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.netty.codec.http</name>
                  </moduleInfo>
                </module>
                <module>
                  <artifact>
                    <groupId>io.vertx</groupId>
                    <artifactId>vertx-core</artifactId>
                  </artifact>
                  <moduleInfo>
                    <name>io.vertx.core</name>
                    <requires>
                      static log4j.api;
                      static log4j;
                      static slf4j.api;
                      *;
                    </requires>
                    <exports>
                      !*impl*;
                      *;
                    </exports>
                    <uses>
                      io.vertx.core.spi.VertxFactory;
                      io.vertx.core.spi.VerticleFactory;
                      io.vertx.core.spi.FutureFactory;
                      io.vertx.core.spi.BufferFactory;
                    </uses>
                  </moduleInfo>
                </module>
              </modules>
              <module>
                <mainClass>com.example.HelloWorldServer</mainClass>
                <moduleInfo>
                  <name>com.example</name>
                  <exports>
                    com.example to io.vertx.core;
                  </exports>
                </moduleInfo>
              </module>
            </configuration>
            <goals>
              <goal>add-module-info</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <profiles>
    <!-- Builds a modular runtime image -->
    <profile>
      <id>jlink</id>
      <build>
        <plugins>
          <plugin>
            <groupId>${project.groupId}</groupId>
            <artifactId>moditect-maven-plugin</artifactId>
            <executions>
              <execution>
                <id>create-runtime-image</id>
                <phase>package</phase>
                <goals>
                  <goal>create-runtime-image</goal>
                </goals>
                <configuration>
                  <modulePath>
                    <path>${project.build.directory}/modules</path>
                  </modulePath>
                  <modules>
                    <module>com.example</module>
                  </modules>
                  <launcher>
                    <name>helloWorld</name>
                    <module>com.example</module>
                  </launcher>
                  <compression>2</compression>
                  <stripDebug>true</stripDebug>
                  <outputDirectory>
                    ${project.build.directory}/jlink-image
                  </outputDirectory>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

Custom runtime images for modular applications

build.gradle for the ModiTect approach (scroll to see more code)
buildscript {
  repositories {
      maven {
        url "https://plugins.gradle.org/m2/"
      }
      maven {
        url 'https://jitpack.io'
      }
  }
  dependencies {
    classpath "org.moditect:moditect-gradle-plugin:1.0.0-beta2"
  }
}

moditect {
  addMainModuleInfo {
    version = project.version
    overwriteExistingFiles = false
    jdepsExtraArgs = ['-q']
    module {
      mainClass = mainClassName
      moduleInfo {
        name = moduleName
        exports = 'com.example to io.vertx.core;'
      }
    }
  }
  addDependenciesModuleInfo {
    jdepsExtraArgs = ['-q']
    modules {
      module {
        artifact "com.fasterxml.jackson.core:jackson-core:$jacksonVersion"
        moduleInfo {
          name = 'com.fasterxml.jackson.core'
        }
      }
      module {
        artifact "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion"
        moduleInfo {
          name = 'com.fasterxml.jackson.annotations'
        }
      }
      module {
        artifact "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
        moduleInfo {
          name = 'com.fasterxml.jackson.databind'
        }
      }
      module {
        artifact "io.netty:netty-common:$nettyVersion"
        moduleInfo {
          name = 'io.netty.common'
        }
      }
      module {
        artifact "io.netty:netty-buffer:$nettyVersion"
        moduleInfo {
          name = 'io.netty.buffer'
        }
      }
      module {
        artifact "io.netty:netty-codec:$nettyVersion"
        moduleInfo {
          name = 'io.netty.codec'
        }
      }
      module {
        artifact "io.netty:netty-resolver:$nettyVersion"
        moduleInfo {
          name = 'io.netty.resolver'
        }
      }
      module {
        artifact "io.netty:netty-transport:$nettyVersion"
        moduleInfo {
          name = 'io.netty.transport'
        }
      }
      module {
        artifact "io.netty:netty-codec-dns:$nettyVersion"
        moduleInfo {
          name = 'io.netty.codec.dns'
        }
      }
      module {
        artifact "io.netty:netty-codec-http2:$nettyVersion"
        moduleInfo {
          name = 'io.netty.codec.http2'
        }
      }
      module {
        artifact "io.netty:netty-resolver-dns:$nettyVersion"
        moduleInfo {
          name = 'io.netty.resolver.dns'
        }
      }
      module {
        artifact "io.netty:netty-transport-native-unix-common:$nettyVersion"
        moduleInfo {
          name = 'io.netty.channel.unix'
        }
      }
      module {
        artifact "io.netty:netty-transport-native-epoll:$nettyVersion"
        moduleInfo {
          name = 'io.netty.channel.epoll'
        }
      }
      module {
        artifact "io.netty:netty-transport-native-kqueue:$nettyVersion"
        moduleInfo {
          name = 'io.netty.channel.kqueue'
        }
      }
      module {
        artifact "io.netty:netty-handler:$nettyVersion"
        moduleInfoSource = '''
          module io.netty.handler {
            exports io.netty.handler.flow;
            exports io.netty.handler.flush;
            exports io.netty.handler.ipfilter;
            exports io.netty.handler.logging;
            exports io.netty.handler.ssl;
            exports io.netty.handler.ssl.ocsp;
            exports io.netty.handler.ssl.util;
            exports io.netty.handler.stream;
            exports io.netty.handler.timeout;
            exports io.netty.handler.traffic;
          }
        '''
      }
      module {
        artifact "io.netty:netty-codec-socks:$nettyVersion"
        moduleInfo {
          name = 'io.netty.codec.socks'
        }
      }
      module {
        artifact "io.netty:netty-handler-proxy:$nettyVersion"
        moduleInfo {
          name = 'io.netty.handler.proxy'
        }
      }
      module {
        artifact "io.netty:netty-codec-http:$nettyVersion"
        moduleInfo {
          name = 'io.netty.codec.http'
        }
      }
      module {
        artifact "io.vertx:vertx-core:$vertxVersion"
        moduleInfo {
          name = 'io.vertx.core'
          requires = '''
            static log4j.api;
            static log4j;
            static slf4j.api;
            *;
          '''
          exports = '''
            !*impl*;
            *;
          '''
          uses = '''
            io.vertx.core.spi.VertxFactory;
            io.vertx.core.spi.VerticleFactory;
            io.vertx.core.spi.FutureFactory;
            io.vertx.core.spi.BufferFactory;
          '''
        }
      }
    }
  }
  createRuntimeImage {
    outputDirectory = file("$buildDir/jlink-image")
    modules = ['com.example']
    launcher {
      name = 'helloWorld'
      module = 'com.example'
    }
    compression = 2
    stripDebug = true
  }
}

Custom runtime images for modular applications

The "badass" approach

  • Combine all non-modular artifacts into a merged module.
  • Modularize the merged module.

 

This approach is taken by the badass-jlink Gradle plugin.

Custom runtime images for modular applications

build.gradle for the "badass" approach
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.5.0"
    id 'org.beryx.jlink' version '2.10.2'
}
repositories { jcenter() }
dependencies {
    implementation "io.vertx:vertx-core:3.5.0"
}
mainClassName = "com.example/com.example.HelloWorldServer"
run.jvmArgs = ["--add-modules", "jdk.unsupported",
                        "--add-reads", "netty.common=jdk.unsupported"]
jlink {
    options = ['--strip-debug', '--compress', '2',
                                      '--no-header-files', '--no-man-pages']
    mergedModule {
        additive = true
        uses 'io.vertx.core.spi.VertxFactory'
        uses 'io.vertx.core.spi.VerticleFactory'
        uses 'io.vertx.core.spi.FutureFactory'
        uses 'io.vertx.core.spi.BufferFactory'
    }
}

Creating application installers with jpackage

jpackage

  • a tool for packaging self-contained Java applications along with a Java Runtime Environment.
  • allows Java applications to be distributed, installed, and uninstalled in a manner that is familiar to users.
  • based on the JavaFX javapackager tool (removed in OpenJDK 11)
  • available in OpenJDK 13+

The badass-jlink plugin provides a jpackage task for creating a platform-specific application installer.

Creating application installers with jpackage

build.gradle
public class HelloFX extends Application {
    @Override
    public void start(Stage stage) {
        Label label = new Label("Hello, JEEConf!");
        label.setFont(Font.font(48));
        StackPane pane = new StackPane(label);
        Scene scene = new Scene(pane, 480, 320);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Creating application installers with jpackage

module-info.java
module org.example.jpms.hellofx {
    requires javafx.controls;
    opens org.example.jpms.hellofx to javafx.graphics;
}

Creating application installers with jpackage

build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.7'
    id 'org.beryx.jlink' version '2.10.2'
}
repositories { jcenter() }
javafx {
    modules = ['javafx.controls']
}
mainClassName = "org.example.jpms.hellofx/org.example.jpms.hellofx.HelloFX"
jlink {
    options = ['--strip-debug', '--compress', '2',
                            '--no-header-files', '--no-man-pages']
    launcher { name = 'helloFX' }
    jpackage {
        if (org.gradle.internal.os.OperatingSystem.current().windows) {
            installerOptions = ['--win-per-user-install', '--win-dir-chooser']
        }
    }
}

Creating application installers with jpackage

build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.7'
    id 'org.beryx.jlink' version '2.10.2'
}
repositories { jcenter() }
javafx {
    modules = ['javafx.controls']
}
mainClassName = "org.example.jpms.hellofx/org.example.jpms.hellofx.HelloFX"
jlink {
    options = ['--strip-debug', '--compress', '2',
                            '--no-header-files', '--no-man-pages']
    launcher { name = 'helloFX' }
    jpackage {
        if (org.gradle.internal.os.OperatingSystem.current().windows) {
            installerOptions = ['--win-per-user-install', '--win-dir-chooser']
        }
    }
}

Creating application installers with jpackage

build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.7'
    id 'org.beryx.jlink' version '2.10.2'
}
repositories { jcenter() }
javafx {
    modules = ['javafx.controls']
}
mainClassName = "org.example.jpms.hellofx/org.example.jpms.hellofx.HelloFX"
jlink {
    options = ['--strip-debug', '--compress', '2',
                            '--no-header-files', '--no-man-pages']
    launcher { name = 'helloFX' }
    jpackage {
        if (org.gradle.internal.os.OperatingSystem.current().windows) {
            installerOptions = ['--win-per-user-install', '--win-dir-chooser']
        }
    }
}

Custom runtime images for non-modular applications

Approach:

  • create a custom runtime image containing only the JDK modules required by the application
     
  • copy all the classes and dependencies along with the start scripts of your application into to the directory of the custom Java runtime image

 
This approach is taken by the badass-runtime Gradle plugin.

Custom runtime images for non-modular applications

build.gradle
plugins {
    id 'application'
    id "org.beryx.runtime" version "1.1.6"
}
repositories {
    jcenter()
}
dependencies {
    implementation "io.vertx:vertx-core:3.5.0"
}

mainClassName = "com.example.HelloWorldServer"

runtime {
    options = ['--strip-debug', '--compress', '2',
                                      '--no-header-files', '--no-man-pages']
    modules = ['java.naming', 'java.compiler', 'java.logging', 'jdk.unsupported']
}

Key takeaways

The Java Module System provides reliable configuration and strong encapsulation

You can mix classpath and module-path

You can create modular artifacts targeted at Java 8 or earlier versions

jlink

  • you can create custom runtime images with only the modules required by your application
  • you can create your own JRE

jpackage

  • you can create platform-specific application installers
  • especially useful for GUI applications

Use Maven or Gradle to build your modular code!

Resources

Code and slides of this talk: https://github.com/beryx/talk-jeeconf-2019

Maven

  • Generate and add module descriptors, create modular JARs targeting Java 8 or older, create custom runtime images of modular applications: ModiTect

Gradle