Author avatar

Esteban Herrera

Getting Started with the Java Platform Module System Part 2

Esteban Herrera

  • Sep 25, 2018
  • 12 Min read
  • 15,932 Views
  • Sep 25, 2018
  • 12 Min read
  • 15,932 Views
Java
J2EE

Introduction

In the previous guide, Getting Started with the Java Platform Module System Part 1, I introduced the concepts of module systems within Java. In this guide, I'll show you more examples for working with modules and how to use ServiceLoader with modules.

Working with Modules

Let's add a simple graphic interface to our quote generator as another module.

Create another top-level directory com.example.gui with the following class:

1package com.example.gui;
2
3import javafx.application.Application;
4import javafx.scene.*;
5import javafx.stage.Stage;
6
7public class QuoteFxApp extends Application {
8
9    @Override
10    public void start(Stage primaryStage) throws Exception {
11        primaryStage.setTitle("Quotes");
12
13        Label label = new Label("A quote");
14        Scene scene = new Scene(label, 500, 200);
15        primaryStage.setScene(scene);
16
17        primaryStage.show();
18    }
19
20    public static void main(String[] args) {
21        Application.launch(args);
22    }
23}
java

And the next module-info.java file:

1module com.example.gui {
2}

When you execute javac to compile the files:

1javac -d out module-info.java com/example/gui/QuoteFxApp.java

These errors will show up:

1com\example\gui\QuoteFxApp.java:3: error: package javafx.application is not visible
2import javafx.application.Application;
3             ^
4  (package javafx.application is declared in module javafx.graphics, but module com.example.gui does not read it)
5com\example\gui\QuoteFxApp.java:4: error: package javafx.scene is not visible
6import javafx.scene.Scene;
7             ^
8  (package javafx.scene is declared in module javafx.graphics, but module com.example.gui does not read it)
9com\example\gui\QuoteFxApp.java:5: error: package javafx.scene.control is not visible
10import javafx.scene.control.Label;
11                   ^
12  (package javafx.scene.control is declared in module javafx.controls, but module com.example.gui does not read it)
13com\example\gui\QuoteFxApp.java:6: error: package javafx.stage is not visible
14import javafx.stage.Stage;
15             ^
16  (package javafx.stage is declared in module javafx.graphics, but module com.example.gui does not read it)
17com\example\gui\QuoteFxApp.java:10: error: method does not override or implement a method from a supertype
18    @Override
19    ^
205 errors

The JavaFX packages are not visible. We need to reference the modules javafx.graphics and javafx.controls. But why?

The reason behind this is that the JDK has now become modularized and our application needs to declare which JDK modules it needs.

But why we didn't get any errors in the previous example? We didn't get any errors because the classes we used were included in the java.base module, which is added by default to all modular applications.

Notice that this only happens in modular applications. Java 9 is backward compatible; if you don't include a module-info.java file in your application, everything will work like in previous versions of Java. If you compile the application without module-info.java, you'll find that no errors are thrown:

1javac -d out com/example/gui/QuoteFxApp.java

But when working modules, we need to require the JavaFX modules our application needs in module-info.java:

1module com.example.gui {
2  requires javafx.controls;
3}

This time, javac will execute successfully.

However, notice that we only required javafx.controls, not javafx.graphics as the error messages mentioned.

That's because if you use the javafx.controls module, most likely, you'll use the javafx.graphics too, so javafx.controls declares this module as a transitive dependency:

1module javafx.controls {
2  ...
3  requires transitive javafx.graphics;
4  ...
5}

Of course, you can explicitly add requires javafx.graphics to your module-info.java file but it's not required, you get it automatically.

By the way, if you want to see all the modules of the JDK, the JDK installation directory contains a subdirectory named jmods. Conveniently, we can use the jmod tool to list the content of these JMOD files.

On my Windows machine, to see the details of the javafx.controls module, I execute:

1jmod describe "C:\Program Files\Java\jdk-9\jmods\javafx.controls.jmod"

Moving on, our problems are not finished. If we run the program with:

1java --module-path out --module com.example.gui/com.example.gui.QuoteFxApp

We'll get the following error:

1java.lang.IllegalAccessException: class com.sun.javafx.application.LauncherImpl (in module javafx.graphics) cannot access class com.example.gui.QuoteFxApp (in module com.example.gui) because module com.example.gui does not export com.example.gui to module javafx.graphics

JavaFX needs access to our class QuoteFxApp, so we need to grant access explicitly by exporting the package com.example.gui this way:

1module com.example.gui {
2  ...
3  
4  exports com.example.gui;
5}

However, this is just how JavaFX works and we probably don't want any other modules to access our package. For cases like this, we can use a qualified export:

1module com.example.gui {
2  ...
3  
4  exports com.example.gui to javafx.graphics;
5}

This way, only the listed modules (in this case onlyjavafx.graphics) can access the package com.example.gui. Our application should work now:

first run JavaFX

Having learned how to require/export other modules, integrating the programming quotes module and the GUI module should be easy.

First, in the module com.example.programming, export its package:

1module com.example.programming {
2  exports com.example.programming;
3}
java

Compile the classes and package them as a JAR file once again. We can copy this JAR to a lib directory for easy access.

Next, modify the class QuoteFxApp to use ProgrammingQuotes:

1...
2import com.example.programming.ProgrammingQuotes;
3...
4public class QuoteFxApp extends Application {
5
6    @Override
7    public void start(Stage primaryStage) throws Exception {
8        ...
9
10        Label label = new Label(new ProgrammingQuotes().getQuote());
11        ...
12    }
13
14    ...
15}
java

Require the com.example.programming module:

1module com.example.gui {
2  ...
3  
4  ...
5}

Compile the com.example.gui module with the module path option so it can locate the com.example.programming module (which in this case, it's placed in the lib directory):

1javac -d out --module-path ../lib module-info.java com/example/gui/QuoteFxApp.java

Finally, run the application using, once again, the module path (remember, the path separator character for Windows is ;. Use : for Linux/Mac):

1java --module-path out;../lib --module com.example.gui/com.example.gui.QuoteFxApp

second run JavaFX

Notice that if you omit the lib directory from the module path, the application won't run:

1java --module-path out --module com.example.gui/com.example.gui.QuoteFxApp
2Error occurred during initialization of boot layer
3java.lang.module.FindException: Module com.example.programming not found, required by com.example.gui

This solves, in part, the JAR/classpath hell problem.

Using Services with the ServiceLoader

Let's say that now we want to show math quotes, in addition to programming quotes. We could create another module for math quotes and modify the GUI module to require it.

We can also create an interface for both types of quotes. In the Java Platform Module System, this will give us the possibility to abstract the mechanism for matching up service interfaces with implementations using the service locator pattern.

This works by using the service-provider loading facility that Java provides with the ServiceLoader class.

To use this mechanism, you need an interface, abstract class, or even a concrete class to act as the type of service, an implementation or subclass, and use the class ServiceLoader to load all the implementations found.

Traditionally, this mechanism uses configuration files in the META-INF/services directory of the JAR to register implementations but, in the JPMS, this is done in the module-info.java file. Let's see it in action.

Create a module for the interface (generally, this module will contain all of your API classes):

1package com.example.quote;
2
3public interface Quote {
4    String getQuote();
5}
java

Export the interface's package in the module-info.java file:

1module com.example.quote {
2  exports com.example.quote;
3}
java

Compile and package as usual:

1# Compilation
2javac -d out module-info.java com/example/quote/Quote.java
3
4# Packaging (placing the jar in the common lib directory)
5jar cvf ../lib/quote.jar -C out .
java

Now modify the ProgrammingQuotes class to implement the interface:

1...
2import com.example.quote.Quote;
3
4public class ProgrammingQuotes implements Quote {
5    ...
6}
java

Don't forget to add the interface module to the module-info.java file:

1module com.example.programming {
2  requires com.example.quote;
3  ...
4}
java

And now, instead of exporting the package of the module, with the help of provides with you can indicate that this module provides an implementation of the Quote interface:

1module com.example.programming {
2  requires com.example.quote;
3  
4  provides com.example.quote.Quote
5    with com.example.programming.ProgrammingQuotes;
6}
java

Next, compile and package:

1# Compilation
2javac -d out --module-path ../lib module-info.java com/example/programming/ProgrammingQuotes.java
3
4# Packaging (placing the jar in the common lib directory)
5jar cvf ../lib/programming-quote.jar -C out .

Notice that we're not exporting the package of this module. We're just declaring the interface and its implementation.

The implementation is encapsulated in a non-exported package. This is generally the case because the point is to hide implementation details. The consumer only knows about the interface, not the implementation.

Speaking of the consumer, in the GUI module, you use the ServiceLoader class like this:

1...
2
3import com.example.quote.Quote;
4import java.util.ServiceLoader;
5
6public class QuoteFxApp extends Application {
7
8    @Override
9    public void start(Stage primaryStage) throws Exception {
10        primaryStage.setTitle("Quotes");
11        
12        Label label = new Label("NO QUOTE");
13
14        ServiceLoader.load(Quote.class)
15                .forEach(service -> label.setText(service.getQuote()));
16
17        Scene scene = new Scene(label, 500, 200);
18        ...
19    }
20
21    ...
22}
java

ServiceLoader.load(Class<T>) returns a ServiceLoader instance that implements Iterable. So, we can iterate over all the discovered implementations of the provided interface.

Next, in the module-info.java file, you just have to indicate that the module will require the module that contains the interface and it will use its implementations:

1module com.example.gui{
2  ...
3  requires com.example.quote;
4  
5  uses com.example.quote.Quote;
6}
java

Compile and run like before:

1# Compile
2javac -d out --module-path ../lib module-info.java com/example/gui/QuoteFxApp.java
3
4# Run (remember to change the path separator if needed)
5java --module-path out;../lib --module com.example.gui/com.example.gui.QuoteFxApp

The app will work as before but this time it doesn't depend on the implementation, just the interface. If you want to add another implementation, just drop another module like com.example.programming into the lib directory and it will be discovered by the ServiceLoader class automatically.

Next Steps

In the next guide, Getting Started with the Java Platform Module System Part 3, I'll talk about how Maven works with modules.