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:
The following are the contents of the module descriptor file called module-info.java:
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:
The following are the contents of the class Main:
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:
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:
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:
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:
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:
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:
The following are the contents of the module descriptor file for my.contact :
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:
The following are the contents of the class Main:
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:
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:
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:
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:
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:
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:
The following are the contents of the module descriptor file for my.rolodex :
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:
The following are the contents of the class Main:
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:
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:
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:
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:
commons.logging java.base my.address my.contact
References