Pluralsight Logo
Author avatar

Esteban Herrera

Author badge Author

Getting Started with the Java Platform Module System Part 2

Esteban Herrera

Author BadgeAuthor
  • Sep 25, 2018
  • 12 Min read
  • 8,050 Views
  • Sep 25, 2018
  • 12 Min read
  • 8,050 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.gui;

import javafx.application.Application;
import javafx.scene.*;
import javafx.stage.Stage;

public class QuoteFxApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Quotes");

        Label label = new Label("A quote");
        Scene scene = new Scene(label, 500, 200);
        primaryStage.setScene(scene);

        primaryStage.show();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}
java

And the next module-info.java file:

1
2
module com.example.gui {
}

When you execute javac to compile the files:

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

These errors will show up:

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

1
javac -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:

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

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:

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

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:

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

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

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

We'll get the following error:

1
java.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:

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

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:

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

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:

1
2
3
module com.example.programming {
  exports com.example.programming;
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
import com.example.programming.ProgrammingQuotes;
...
public class QuoteFxApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        ...

        Label label = new Label(new ProgrammingQuotes().getQuote());
        ...
    }

    ...
}
java

Require the com.example.programming module:

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

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):

1
javac -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):

1
java --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:

1
2
3
java --module-path out --module com.example.gui/com.example.gui.QuoteFxApp
Error occurred during initialization of boot layer
java.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):

1
2
3
4
5
package com.example.quote;

public interface Quote {
    String getQuote();
}
java

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

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

Compile and package as usual:

1
2
3
4
5
# Compilation
javac -d out module-info.java com/example/quote/Quote.java

# Packaging (placing the jar in the common lib directory)
jar cvf ../lib/quote.jar -C out .
java

Now modify the ProgrammingQuotes class to implement the interface:

1
2
3
4
5
6
...
import com.example.quote.Quote;

public class ProgrammingQuotes implements Quote {
    ...
}
java

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

1
2
3
4
module com.example.programming {
  requires com.example.quote;
  ...
}
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:

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

Next, compile and package:

1
2
3
4
5
# Compilation
javac -d out --module-path ../lib module-info.java com/example/programming/ProgrammingQuotes.java

# Packaging (placing the jar in the common lib directory)
jar 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...

import com.example.quote.Quote;
import java.util.ServiceLoader;

public class QuoteFxApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Quotes");
        
        Label label = new Label("NO QUOTE");

        ServiceLoader.load(Quote.class)
                .forEach(service -> label.setText(service.getQuote()));

        Scene scene = new Scene(label, 500, 200);
        ...
    }

    ...
}
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:

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

Compile and run like before:

1
2
3
4
5
# Compile
javac -d out --module-path ../lib module-info.java com/example/gui/QuoteFxApp.java

# Run (remember to change the path separator if needed)
java --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.

0