Building Modules on the Command Line
When using the module system to create modules for your code, you will likely do that in a project that uses a build tool and so it is its task to get things right.
But it helps tremendously to understand what "right" looks like and how to correctly configure javac, jar, and java to compile, package, and run your application.
This will give you a better understanding of the module system and help debug problems in case the build tool doesn't get it right.
Note: You need to know the module system basics to get the most out of this article. You may also want to check out the description of the core JDK tools.
A Basic Build
Given a project with a few source files, a module declaration, and a few dependencies, this is how you can compile, package, and run it in the simplest way:
# compile sources files, including module-info.java
$ javac
--module-path $DEPS
-d $CLASS_FOLDER
$SOURCES
# package class files, including module-info.class
$ jar --create
--file $JAR
$CLASSES
# run by specifying a module by name
$ java
--module-path $JAR:$DEPS
--module $MODULE_NAME/$MAIN_CLASS
There's a bunch of placeholders in there:
$DEPSis the list of dependencies. These are typically paths to JAR files separated by:(Unix) or;(Windows), but on the module path, this can also just be folder names (without the/*-trickery that's required on the class path).$CLASS_FOLDERis the path to the folder where the*.classfiles will be written to.$SOURCESis the list of*.javafiles and must includemodule-info.java.$JARis the path to the JAR file that will be created.$CLASSESis the list of*.classfiles that was created during compilation (thus found in$CLASS_FOLDER) and must includemodule-info.class.$MODULE_NAME/$MAIN_CLASSis the name of the initial module (i.e. the one where module resolution starts) followed by the name of the class containing the app'smainmethod.
For a simple "Hello World" style project with the common src/main/java structure, just a single source file, dependencies in a deps folder, and using Maven's target folder that would look as follows:
$ javac
--module-path deps
-d target/classes
src/main/java/module-info.java
src/main/java/com/example/Main.java
$ jar --create
--file target/hello-modules.jar
target/classes/module-info.class
target/classes/com/example/Main.class
$ java
--module-path target/hello-modules.jar:deps
--module com.example/com.example.Main
Defining a Main Class
The jar option --main-class $MAIN_CLASS embeds $MAIN_CLASS as the class containing the main method in the module descriptor, which allows you to launch a module without having to name the main class:
$ jar --create
--file target/hello-modules.jar
--main-class com.example.Main
target/classes/module-info.class
target/classes/com/example/Main.class
$ java
--module-path target/hello-modules.jar:deps
--module com.example
Note that it is possible to override that class and launch another, simply by naming it as before:
# create a JAR with `Main` and `Side`,
# making `Main` the main class
$ jar --create
--file target/hello-modules.jar
--main-class com.example.Main
target/classes/module-info.class
target/classes/com/example/Main.class
target/classes/com/example/Side.class
# override the main class and launch `Side`
$ java
--module-path target/hello-modules.jar:deps
--module com.example/com.example.Side
Circumventing Strong Encapsulation
The module system is very strict about access to internal APIs:
If the package isn't exported or opened, access will be denied.
But a package can't just be exported or opened by a module's author - there are also the command line flags --add-exports and --add-opens, which allow the module's user to do that as well.
As an example, see this code that tries to create an instance of the internal class sun.util.BuddhistCalendar:
BuddhistCalendar calendar = new BuddhistCalendar();
To compile and run it, we need to use --add-exports:
javac
--add-exports java.base/sun.util=com.example.internal
module-info.java Internal.java
# package with `jar`
java
--add-exports java.base/sun.util=com.example.internal
--module-path com.example.internal.jar
--module com.example.internal
If the access is reflective...
Class.forName("sun.util.BuddhistCalendar").getConstructor().newInstance();
... compilation will work without further configuration, but we need to add --add-opens when running the code:
java
--add-opens java.base/sun.util=com.example.internal
--module-path com.example.internal.jar
--module com.example.internal
Details on strong encapsulation and circumventing it with add-exports and add-opens.
Extending the Module Graph
Starting with an initial set of root modules, the module system computes all of their dependencies and builds a graph, where the modules are nodes and their readability relations are directed edges.
This module graph can be extended with the command line flags --add-modules and --add-reads, which add modules (and their dependencies) and readability edges, respectively.
As an example, let's imagine a project that has an optional dependency on java.sql, but the module is not otherwise required. That means it's not added to the module graph without a little help:
# launch without java.sql
$ java
--module-path example.jar:deps
--module com.example/com.example.Main
# launch with java.sql
$ java
--module-path example.jar:deps
--add-modules java.sql
--module com.example/com.example.Main
An alternative approach to optional dependencies would be to not list the dependency at all and only add it with --add-modules and --add-reads (this is rarely helpful and not generally recommended - just an example):
$ java
--module-path example.jar:deps
--add-modules java.sql
--add-reads com.example=java.sql
--module com.example/com.example.Main
Details on extending the module graph with --add-modules and --add-reads.
Last update: September 14, 2021