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.
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}
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:
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}
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}
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
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.
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}
Export the interface's package in the module-info.java
file:
1module com.example.quote {
2 exports com.example.quote;
3}
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 .
Now modify the ProgrammingQuotes
class to implement the interface:
1...
2import com.example.quote.Quote;
3
4public class ProgrammingQuotes implements Quote {
5 ...
6}
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}
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}
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}
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}
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.
In the next guide, Getting Started with the Java Platform Module System Part 3, I'll talk about how Maven works with modules.