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, 12, 13)

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

gradle
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

gradle
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\nExecuted on the module-path: %b\n",
                            XMLPrinter.class.getModule().isNamed());
    }
}

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\nExecuted on the module-path: %b\n",
                            ProductTest.class.getModule().isNamed());
    }
}

Building a modular application with Gradle

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

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

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

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'))
    }
}

Support for the Java Module System

maven

 MAVEN

  • maven-compiler-plugin since version 3.7.0 (September 1, 2017)

 

gradle

 GRADLE

The gradle-modules-plugin

Introduced in September 2018 by Paul Bakker & Sander Mak

 

Features:

  • compiling modules
  • running/packaging modular applications using the application plugin
  • testing modules with both whitebox and blackbox tests
  • support for the most popular test engines (JUnit 5, JUnit 4, TestNG)
  • support for Kotlin
  • support for Groovy 3
modularity book

Using Gradle with the gradle-modules-plugin

gradle
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.6.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
}

Configuring additional VM arguments for testing

gradle
ProductTest.java
package org.example.jpms;

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

import java.util.logging.Logger;

public class ProductTest {
    @Test
    public void testToString() {
        Product p = new Product(33, "spaghetti", 2.15);
        Logger.getGlobal().info("test product: " + p);
        Assertions.assertEquals("spaghetti: 2.15", p.toString());
        System.out.printf("\n\nExecuted on the module-path: %b\n",
                            ProductTest.class.getModule().isNamed());
    }
}

Configuring additional VM arguments for testing

gradle

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

Configuring additional VM arguments for testing

gradle

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

module info.test

module-info.test
--add-modules
  java.logging

--add-reads
  org.example.jpms=java.logging

build.gradle
test {
  moduleOptions {
    runOnClasspath = true
  }
}

Modular JARs compatible with Java 8 (or earlier)

The standard approach

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 gradle-modules-plugin.

Modular JARs compatible with Java 8 (or earlier)

gradle
build.gradle
plugins {
    id 'java'
    id "org.javamodularity.moduleplugin" version "1.6.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
}

Modular JARs compatible with Java 8 (or earlier)

maven

Using Gunnar Morling’s ModiTect

  • Generating module-info.java descriptors for given artifacts
  • Adding module descriptors to your project’s JAR as well as existing JAR files
  • Creating module runtime images

 

The ModiTect approach

  • analyze the module-info.java file with the JavaParser
  • use the ASM bytecode manipulation framework to generate the corresponding module descriptor

 

Modular JARs compatible with Java 8 (or earlier)

maven
pom.xml
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.beta3</version>
<executions>
    <execution>
        <id>add-module-infos</id>
        <phase>package</phase>
        <goals><goal>add-module-info</goal></goals>
        <configuration>
            <jvmVersion>8</jvmVersion>
            <module>

                <moduleInfoFile>
                	src/main/java/module-info.java
                </moduleInfoFile>


            </module>
        </configuration>
    </execution>
</executions>

Modular JARs compatible with Java 8 (or earlier)

maven
pom.xml
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.beta3</version>
<executions>
    <execution>
        <id>add-module-infos</id>
        <phase>package</phase>
        <goals><goal>add-module-info</goal></goals>
        <configuration>
            <jvmVersion>8</jvmVersion>
            <module>
                <moduleInfoSource>
                    module org.example.jax {
                        requires java.sql;
                        exports org.example.jax;
                    }
                </moduleInfoSource>
            </module>
        </configuration>
    </execution>
</executions>

Modular JARs compatible with Java 8 (or earlier)

gradle

Using the ModiTect approach with the org.beryx.jar Gradle plugin

build.gradle
plugins {
    id 'java'
    id 'org.beryx.jar' version '1.1.3'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

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

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

}
repositories { jcenter() }
dependencies {
    implementation "io.vertx:vertx-core:3.5.0"
}
mainClassName = "com.example/com.example.HelloWorldServer"

Custom runtime images for modular applications

gradle
build.gradle
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.6.0"
    id "org.kordamp.gradle.jdeps" version "0.6.0"
}
repositories { jcenter() }
dependencies {
    implementation "io.vertx:vertx-core:3.5.0"
}
mainClassName = "com.example/com.example.HelloWorldServer"

Custom runtime images for modular applications

gradle
build.gradle
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.6.0"
    id "org.kordamp.gradle.jdeps" version "0.6.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

maven
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

gradle
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-beta3"
  }
}


plugins{
    id "java"
    id "application"
}
apply plugin: "org.moditect.gradleplugin"

repositories {
    mavenCentral()
}

group = "org.moditect"
version = "2.0.0"

targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_11

ext {
    moduleName = 'com.example'

    vertxVersion = '3.5.0'
    nettyVersion = '4.1.15.Final'
    jacksonVersion ='2.9.0'
}

mainClassName = 'com.example.HelloWorldServer'
jar {
    manifest {
        attributes("Automatic-Module-Name": moduleName)
    }
}

dependencies{
    compile "io.vertx:vertx-core:$vertxVersion"
}

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

gradle
build.gradle for the "badass" approach
plugins {
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.6.0"
    id 'org.beryx.jlink' version '2.16.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 14+

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

Creating application installers with jpackage

gradle
build.gradle
public class HelloFX extends Application {
    @Override
    public void start(Stage stage) {
        Label label = new Label("Hello, W-JAX!");
        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

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

Creating application installers with jpackage

gradle
build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.beryx.jlink' version '2.16.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', '--win-menu']
        }
    }
}

Creating application installers with jpackage

gradle
build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.beryx.jlink' version '2.16.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', '--win-menu']
        }
    }
}

Creating application installers with jpackage

gradle
build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.beryx.jlink' version '2.16.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', '--win-menu']
        }
    }
}

Custom runtime images for non-modular applications

Approach:

  • create a custom JRE 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

gradle
build.gradle
plugins {
    id 'application'
    id "org.beryx.runtime" version "1.7.1"
}
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']
}

Resources

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

maven

    Maven

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

    Gradle