PolarSPARC

Introduction to the Java Platform Module System - Part 2


Bhaskar S 05/31/2020


In Part 1 of this series, we introduced the basics of the Java Platform Module System and got our hands dirty with some examples.

In this part, we will demonstrate how one can build modular Java application through simple examples.

A module is a container for one or more Java package(s), resource file(s) such as configuration file(s), and a descriptor.

The module descriptor is a special file called module-info.java that resides at the root of the module source directory.

For our first example, let us define a module that packages an Address entity. The module source directory will be called my.address.

The following diagram illustrates the directory structure and contents of our module:

Module Structure
Figure.6

The following are the contents of the module descriptor file called module-info.java:

Listing.4
module my.address {
    exports com.polarsparc.address;
}

The module starts with the keywork module followed by the module name. The module name *MUST* match the name of the module source directory, which is my.address in our example.

The exports keyword indicates the name of the Java package that is exposed for external consumption from the other modules. In our example, we have exported all the class(es) from the package com.polarsparc.address.

The following are the contents of the Address class:

Listing.5
package com.polarsparc.address;

public class Address {
    private String _no;
    private String _street;
    private String _state;
    private String _zip;
    
    public Address(String no, String street, String state, String zip) {
        this._no = no;
        this._street = street;
        this._state = state;
        this._zip = zip;
    }
    
    public String getNo() {
        return this._no;
    }
    
    public String getStreet() {
        return this._street;
    }
    
    public String getState() {
        return this._state;
    }
    
    public String getZip() {
        return this._zip;
    }
    
    public String getAddress() {
        return String.format("%s %s, %s %s", this._no, this._street, this._state, this._zip);
    }
}

The following are the contents of the class Main:

Listing.6
package com.polarsparc.address;

import java.util.logging.Logger;

public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Main.class.getName());

        Address addr = new Address("10", "Martian Blvd", "Mars", "00001");
        
        logger.info(addr.getAddress());
    }
}

To compile our module using the Java 11 compiler, execute the following command:

$ javac -d target/my.address src/my.address/module-info.java src/my.address/main/java/com/polarsparc/address/*.java

The following will be the typical output:

Output.10

src/my.address/main/java/com/polarsparc/address/Main.java:4: error: package java.util.logging is not visible
import java.util.logging.Logger;
                ^
  (package java.util.logging is declared in module java.logging, but module my.address does not read it)
1 error

Compilation FAILED ??? What happened here ???

From Output.10 above, we see the module named my.address is trying to use the package java.util.logging in the class Main and we have *NOT* indicated that dependency in the module descriptor file.

The following are the fixed contents of the module descriptor file:

Listing.7
module my.address {
    exports com.polarsparc.address;

    requires java.logging;
}

The requires keyword indicates the dependency on the module named java.logging.

Now the compilation will succeed !!!

To execute the class Main from our module, execute the following command:

$ java --module-path target --module my.address/com.polarsparc.address.Main

Notice the use of the command-line option --module-path to indicate the location of the user-defined module. The --module-path option takes a list of paths to directories containing compiled class(es) or JAR file(s) for modules.

The following will be the typical output:

Output.11

May 31, 2020 9:11:21 AM com.polarsparc.address.Main main
INFO: 10 Martian Blvd, Mars 00001

One can also package the module into a JAR file to make it distributable.

To create a JAR file from our module, execute the following command:

$ jar --create --file mods/myaddress.jar --main-class com.polarsparc.address.Main -C target/my.address .

This will create a JAR file called myaddress.jar in the mods directory.

To execute the class Main from myaddress.jar, execute the following command:

$ java --module-path mods --module my.address

The following will be the typical output:

Output.12

May 31, 2020 9:13:03 AM com.polarsparc.address.Main main
INFO: 10 Martian Blvd, Mars 00001

To list all the module dependencies for our custom module that is packaged as myaddress.jar, execute the following command:

$ jdeps --list-deps mods/myaddress.jar

The following will be the typical output:

Output.13

   java.base
   java.logging

For our second example, let us define a module that packages a Contact entity, which uses the Address entity from the module named my.address. The module source directory will be called my.contact.

The following diagram illustrates the directory structure and contents of our next module:

Module Structure
Figure.7

The following are the contents of the module descriptor file for my.contact :

Listing.8
module my.contact {
    exports com.polarsparc.contact;
    
    requires transitive my.address;
    requires java.logging;
}

Notice the use of the keyword requires transitive in the listing above. The transitive keyword indicates that any module that depends on my.contact does *NOT* have to explicitly indicate the dependency on my.address. It is automatically implied as a result of this keyword.

The following are the contents of the Contact class:

Listing.9
package com.polarsparc.contact;

import com.polarsparc.address.Address;

public class Contact {
    private String _name;
    private Address _address;
    
    public Contact(String name, Address address) {
        this._name = name;
        this._address = address;
    }
    
    public String getName() {
        return this._name;
    }
    
    public Address getAddress() {
        return this._address;
    }
    
    public String getContact() {
        return String.format("%s => %s", this._name, this._address.getAddress());
    }
}

The following are the contents of the class Main:

Listing.10
package com.polarsparc.contact;

import java.util.logging.Logger;
import com.polarsparc.address.Address;

public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Main.class.getName());

        Address address = new Address("20", "Pluto Street", "Pluto", "00002");
        
        Contact contact = new Contact("Vader", address);
        
        logger.info(contact.getContact());
    }
}

To compile our module using the Java 11 compiler, execute the following command:

$ javac -d target/my.contact --module-path target src/my.contact/module-info.java src/my.contact/main/java/com/polarsparc/contact/*.java

To execute the class Main from my.contact, execute the following command:

$ java --module-path target --module my.contact/com.polarsparc.contact.Main

The following will be the typical output:

Output.14

May 31, 2020 9:16:15 AM com.polarsparc.contact.Main main
INFO: Vader => 20 Pluto Street, Pluto 00002

Let us make some tweaks to the Main class from Listing.10 above, to access the private field _state (using reflection) from the object instance address.

The following are the modified contents of the class Main:

Listing.11
package com.polarsparc.contact;

import java.lang.reflect.Field;
import java.util.logging.Logger;
import com.polarsparc.address.Address;

public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Main.class.getName());

        Address address = new Address("20", "Pluto Street", "Pluto", "00002");
        
        Contact contact = new Contact("Vader", address);
        
        logger.info(contact.getContact());

        try {
            Class<?> clazz = address.getClass();
            Field f_state = clazz.getDeclaredField("_state");
            f_state.setAccessible(true);
            Object val = f_state.get(address);
            logger.info("f_state: " + val.toString());
        }
        catch (Exception ex) {
            System.err.println(ex);
        }
    }
}

Re-compile the module for my.contact, and re-execute the class Main using the following command:

$ java --module-path target --module my.contact/com.polarsparc.contact.Main

The following will be the typical output:

Output.15

May 31, 2020 9:21:27 AM com.polarsparc.contact.Main main
INFO: Vader => 20 Pluto Street, Pluto 00002
java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String com.polarsparc.address.Address._state accessible: module my.address does not "opens com.polarsparc.address" to module my.contact

Because of strong encapsulation enforcement in modules, access to private members (via reflection) is prevented. As the error from Output.15 above indicates, one needs to grant access to a package for reflection. This is done using the keyword opens in the descriptor file of my.address.

The following are the modified contents of the descriptor file for my.address:

Listing.12
module my.address {
    exports com.polarsparc.address;

    opens com.polarsparc.address;

    requires java.logging;
}

First, re-compile the module for my.address, next re-compile the module for my.contact, and re-execute the class Main using the following command:

$ java --module-path target --module my.contact/com.polarsparc.contact.Main

The following will be the typical output:

Output.16

May 31, 2020 9:28:42 AM com.polarsparc.contact.Main main
INFO: Vader => 20 Pluto Street, Pluto 00002
May 31, 2020 9:28:42 AM com.polarsparc.contact.Main main
INFO: f_state: Pluto

For our third example, let us define a module that packages a Rolodex entity, which needs both the Address and Contact entities from the modules named my.address and my.contact respectively. The module source directory will be called my.rolodex.

We will make it a little interesting for this demonstration - the class Main will leverage the non-modular Apache Commons Logging instead of the built-in logging.

The following diagram illustrates the directory structure and contents of our next module:

Module Structure
Figure.8

The following are the contents of the module descriptor file for my.rolodex :

Listing.13
module my.rolodex {
    requires my.contact;
}

Because we used the transitive keyword when describing my.contact, we *DO NOT* have to indicate the dependence on my.address.

The following are the contents of the Rolodex class:

Listing.14
package com.polarsparc.rolodex;

import java.util.Map;
import java.util.HashMap;

import com.polarsparc.contact.Contact;

public class Rolodex {
    private Map _contacts;
    
    public Rolodex() {
        this._contacts = new HashMap<>();
    }
    
    public Contact getContact(String name) {
        return this._contacts.get(name.toLowerCase());
    }
    
    public void addContact(Contact contact) {
        this._contacts.put(contact.getName().toLowerCase(), contact);
    }
}

The following are the contents of the class Main:

Listing.15
package com.polarsparc.rolodex;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.polarsparc.address.Address;
import com.polarsparc.contact.Contact;

public class Main {
    public static void main(String[] args) {
        Log logger = LogFactory.getLog(Main.class);

        Rolodex rolodex = new Rolodex();

        Address address0 = new Address("10", "Mars Blvd", "Mars", "00001");
        Contact contact0 = new Contact("Yoda", address0);
        rolodex.addContact(contact0);

        Address address1 = new Address("20", "Pluto Street", "Pluto", "00002");
        Contact contact1 = new Contact("Vader", address1);
        rolodex.addContact(contact1);
        
        logger.info(rolodex.getContact("yoda").getContact());
    }
}

To compile our module using the Java 11 compiler, execute the following command:

$ javac -d target/my.rolodex --module-path lib:target src/my.rolodex/module-info.java src/my.rolodex/main/java/com/polarsparc/rolodex/*.java

The following will be the typical output:

Output.17

src/my.rolodex/main/java/com/polarsparc/rolodex/Main.java:4: error: package org.apache.commons.logging is not visible
import org.apache.commons.logging.Log;
                         ^
  (package org.apache.commons.logging is declared in the unnamed module, but module org.apache.commons.logging does not read it)
src/my.rolodex/main/java/com/polarsparc/rolodex/Main.java:5: error: package org.apache.commons.logging is not visible
import org.apache.commons.logging.LogFactory;
                         ^
  (package org.apache.commons.logging is declared in the unnamed module, but module org.apache.commons.logging does not read it)
2 errors

Okay - we know that we did not include the requires keyword for commons-logging-1.2.jar. It is *NOT* a module. So, what do we do here ???

A JAR on the module path is treated like a module and is referred to as an Automatic module. The name of an automatic module is derived from the name of the JAR file - all the dashes ('-') are replaced with dots ('.'), the version number is dropped, and any trailing dot ('.') is omitted.

An automatic module exports all of its Java packages and grants transitive readability on the other automatic module(s) it depends on.

In our example, the JAR file commons-logging-1.2.jar assumes the module name of commons.logging.

The following are the fixed contents of the module descriptor file:

Listing.16
module my.rolodex {
    requires my.contact;
    requires commons.logging;
}

Now the compilation will succeed !!!

To execute the class Main from our module, execute the following command:

$ java --module-path lib:target --module my.rolodex/com.polarsparc.rolodex.Main

The following will be the typical output:

Output.18

May 31, 2020 12:13:09 PM com.polarsparc.rolodex.Main main
INFO: Yoda => 10 Mars Blvd, Mars 00001

To list all the module dependencies for our custom module named my.rolodex, execute the following command:

$ jdeps --list-deps --module-path lib:target target/my.rolodex

The following will be the typical output:

Output.19

   commons.logging
   java.base
   my.address
   my.contact

References

Introduction to the Java Platform Module System - Part 1

Modular JDK - Project Jigsaw

The State of the Module System



© PolarSPARC