Getting Started with the Java Platform Module System Part 1
Sep 25, 2018 • 10 Minute Read
Introduction
Java 9 introduced the concept of modules to the Java platform. Modules change the way we design and build Java applications. Even though their use is optional, the JDK itself is now modularized, so we must at least know the basics of the Java Platform Module System (JPMS).
In this guide series, you'll learn what modules are, which problems they solve, how to create them, how they interact with each other, and how we can decouple modules using the service locator pattern. In the final guide of this series, we'll review how Maven works with the module system.
We'll be using command line to compile, package, and run the code examples to better understand what's going on. However, you can use the latest (or beta) version of your favorite IDE to do this. Of course, you'll need to have the Java 9 SDK installed. Download it from here or here and follow the installation instructions.
Let's start by talking about what are modules and what problems they solve.
Why Modules?
In a few words, a module is (generally) a JAR file that contains a set of packages. Just like how fields and methods are grouped into classes and then turn into packages, packages are grouped into modules.
Modularizing an application means decomposing it into multiple modules that work together. The important thing is that with modules, you have to explicitly require other modules your module depends on and you have to export packages from your module to be used by other modules. This brings some benefits and solves three problems.
The Problems Modularizing Solves
First of all, the direct consequence of the module system is that public no longer means everyone can access a type. If a class is marked as public, it can be public only within a module (if its package is not exported) or only to specific modules that require it.
This promotes stronger encapsulation (the ability to hide parts of the code), which is good because we will no longer have access to internal (private) classes or private members (through reflection) that we are not supposed to use but occasionally change between versions and break our code.
Then, we have the problem of classloaders. Traditionally, the Java Virtual Machine (JVM) has used classloaders to load classes specified in the classpath. But what happens if you forget to add a JAR or a class to the classpath and run your application?
Maybe nothing and that's a bad thing. Yes, when the classloader tries to load the missing class, a ClassNotFoundException will be thrown but, since classes are lazily loaded, this may not happen at application startup.
And let's not forget all the problems that can happen when we have duplicate JARs or when two libraries require different versions of a third library. This situation even has a name, JAR (or classpath) hell.
Projects like Maven (at compile-time by helping managing dependencies) and Open Services Gateway initiative (OSGi) (at runtime with JARs annotated with metadata declaring which packages to export and import from other annotated JARs) help us mitigate those classpath problems.
The JPMS doesn't intend to completely replace those projects (we'll talk about Maven in particular in Part 3). Java now has the necessary information to throw not only a compile-time error, if a module is not present or visible, but also a runtime error, when trying to start an application without having access to all the modules it needs or when there's duplicate modules.
The third problem is the deployment size of Java application. Even a simple Hello World program requires a full JRE installation with the rt.jar file (at around 50 MB) that contains almost all the standard Java classes. As said before, in addition to offering a module system for our applications, the JPMS modularizes the JDK itself and, with the help of a tool, jlink, we can create a custom JDK with only the modules we need.
In addition, there's no problem if your application is not yet ready to use modules, the use of modules is optional. Java 9 is backward compatible, if there's no modules information, Java will use the classpath as usual. Besides, an application can even mix a module path and a classpath.
Having explained what a module is and what problems it solves, let's dive into creating our first module.
My First Module
As an example, we are going to use a class with a method that returns a random, predefined quote related to programming:
import java.util.Random;
public class ProgrammingQuotes {
private String[] quotes = new String[] {
"\"To iterate is human, to recurse divine.\"\n" +
"- L. Peter Deutsch",
"\"Don't worry if it doesn't work right. If everything did, you'd be out of a job.\"\n" +
"- Mosher's Law of Software Engineering",
"\"Good design adds value faster than it adds cost.\"\n" +
"- Thomas C. Gale",
"\"Talk is cheap. Show me the code.\"\n" +
"- Linus Torvalds",
"\"I don't care if it works on your machine! We are not shipping your machine!\"\n" +
"- Vidiu Platon",
};
private Random rand = new Random();
private int getRandomIndex() {
return rand.nextInt(quotes.length);
}
public String getQuote() {
return quotes[getRandomIndex()];
}
public static void main(String args[]) {
System.out.println(new ProgrammingQuotes().getQuote());
}
}
First, we have to put our class in a package because a module cannot use the default package (you cannot export an unnamed package).
So let's use the package com.example.programming:
package com.example.programming;
import java.util.Random;
public class ProgrammingQuotes {
// ...
}
Next, we have to choose a name for our module. The recommended approach is to use the same reverse-domain-name pattern that is used for packages. Mark Reinhold, Chief Architect of the Java Platform Group at Oracle, wrote:
"Strongly recommend that all modules be named according to the reverse Internet domain-name convention. A module's name should correspond to the name of its principal exported API package, which should also follow that convention. If a module does not have such a package, or if for legacy reasons it must have a name that does not correspond to one of its exported packages, then its name should at least start with the reversed form of an Internet domain with which the author is associated."
Of course, you can use any naming convention you want. For example, some people use short, project-oriented names.
To make a module, you need to add a module-info.java file under the base directory (where your package directories start). By convention, the name of this base directory is the same as the module name.
This way, the directory structure of our project should be something like this:
|─ com.example.programming
| |─ module-info.java
| |─ com
| |─ example
| |─ programming
| |─ ProgrammingQuotes.java
In the file module-info.java, you can do the following:
- Give your module a name
- Require other modules
- Export packages from your module
In this case, it can be as simple as:
module com.example.programming {
}
Now open a terminal window, make sure to cd into the base directory, and compile as usual:
javac -d out module-info.java com/example/programming/ProgrammingQuotes.java
The directory out will be created with the compiled .class files for module-info.java and ProgrammingQuotes.java.
Next, package the class into a modular JAR:
The only difference with a normal JAR is the presence of the module-info.class file).
jar cvfe programming-quote.jar com.example.programming.ProgrammingQuotes -C out .
This will create the file programming-quote.jar, including all of the classes found in the directory out and with com.example.programming.ProgrammingQuotes as the main class.
Now run it with:
java -jar programming-quote.jar
Of course, all this compilation/packaging/running can be done with an IDE, but I wanted to show you that the commands that do these task haven't changed.
However, to run this program as a module, you have to specify the module path (which contains modules, unlike the classpath that contains classes) with the option --module-path (or just -p) and the main module/class with the option --module (or just -m), in the format module/class:
java --module-path programming-quote.jar --module com.example.programming/com.example.programming.ProgrammingQuotes
Alternatively, you can use the out directory that contains the compile classes:
java --module-path out --module com.example.programming/com.example.programming.ProgrammingQuotes
However, when your application has more than modules, these options become mandatory on the above commands.
In the next guide, Getting Started with the Java Platform Module System Part 2, you'll learn more about modules and how to work with ServiceLoader services.