Java Module System

Overview

Ever since I came across release notes of Java 9 (released in September 2017) I was telling myself to look at the JPMS one day.

It was a product of project Jigsaw. Quote from that site

The primary goals of this Project were to:

  • Make it easier for developers to construct and maintain libraries and large applications;
  • Improve the security and maintainability of Java SE Platform Implementations in general, and the JDK in particular;
  • Enable improved application performance; and
  • Enable the Java SE Platform, and the JDK, to scale down for use in small computing devices and dense cloud deployments.

Experiment

Here is a little project I've created to learn about Java Module System.

I didn't use build tooling such as Maven or Gradle to avoid extra noise and complexity.

The application consists of a few modules, the structure and class relationships are somewhat artificial to make experimentation easier.

 1java-module-system
 2├── abilities
 3│   ├── com
 4│   │   └── sebwalak
 5│   │       └── abilities
 6│   │           ├── suppressed
 7│   │           │   └── Singing.java
 8│   │           ├── Closing.java
 9│   │           ├── Exhaling.java
10│   │           ├── Inhaling.java
11│   │           ├── Opening.java
12│   │           ├── Saying.java
13│   │           └── Translating.java
14│   ├── abilities.iml
15│   └── module-info.java
16├── external.body.parts
17│   ├── com
18│   │   └── sebwalak
19│   │       └── bodyparts
20│   │           ├── external
21│   │           │   └── Mouth.java
22│   │           └── extremities
23│   │               └── Head.java
24│   ├── external.body.parts.iml
25│   └── module-info.java
26├── human
27│   ├── com
28│   │   └── sebwalak
29│   │       ├── species
30│   │       │   └── Human.java
31│   │       └── App.java
32│   ├── human.iml
33│   └── module-info.java
34├── internal.organs
35│   ├── com
36│   │   └── sebwalak
37│   │       └── internalorgans
38│   │           └── Lungs.java
39│   ├── internal.organs.iml
40│   └── module-info.java
41├── knowledge.language.english
42│   ├── com
43│   │   └── sebwalak
44│   │       └── knowledge
45│   │           └── language
46│   │               └── english
47│   │                   └── English.java
48│   ├── knowledge.language.english.iml
49│   └── module-info.java
50├── knowledge.language.spanish
51│   ├── com
52│   │   └── sebwalak
53│   │       └── knowledge
54│   │           └── language
55│   │               └── spanish
56│   │                   └── Spanish.java
57│   ├── knowledge.language.spanish.iml
58│   └── module-info.java
59├── build_all_modules.sh
60├── build.sh
61├── demo.sh
62├── Dockerfile
63├── README.md
64└── run.sh

Each of the following directories is a JPMS module:

  • abilities
  • external.body.parts
  • human
  • internal.organs
  • knowledge.language.english
  • knowledge.language.spanish

Module meta file

What's typical for a module is the presence of a module-info.java file in each module root.

This file lets us name the module, declare module dependencies, export packages, allow reflection access, provide or use a service.

With the Java Module System a type is only accessible across modules if it is public and explicitly exported.

Module abilities specifies:

1module abilities {
2    exports com.sebwalak.abilities;
3    exports com.sebwalak.abilities.suppressed;
4
5    opens com.sebwalak.abilities.suppressed to human;
6}

From module abilities only the types within the package com.sebwalak.abilities and com.sebwalak.abilities.suppressed can be used by other modules (keyword exports).

Each package has to be exported explicitly. There is no wildcards or recursive export.

Keyword opens made types within package com.sebwalak.abilities.suppressed accessible by reflection from within module human.

With Java 9 the reflection access becomes controlled. It allows for stronger encapsulation but is just a default configuration which can be tweaked if you really need it. 🤠

If you want to open entire module to everyone

1open module abilities {
2}

To open types in one package to everyone

1module abilities {
2    opens com.sebwalak.abilities.suppressed;
3}

And there's a command-line JVM option as well:

1java --add-opens abilities/com.sebwalak.abilities.suppressed=ALL-UNNAMED

To make module abilities transitively available to the user of external.body.parts:

1module external.body.parts {
2    requires transitive abilities;
3    ...
4}

To register a service:

1import com.sebwalak.knowledge.language.english.English;
2
3module knowledge.language.english {
4    ...
5    provides com.sebwalak.abilities.Translating
6        with English;
7}

and

1import com.sebwalak.knowledge.language.spanish.Spanish;
2
3module knowledge.language.spanish {
4    ...
5    provides com.sebwalak.abilities.Translating
6        with Spanish;
7}

When I was creating classes English and Spanish I initially placed them both under com.sebwalak.knowledge.language in two separate modules. Soon it became clear that split packages are forbidden in JPMS and I had to devise different package names for them.

and register a module as a service consumer:

1module human {
2    ...
3
4    uses com.sebwalak.abilities.Translating;
5}

Such a service can be used within your application:

1    ServiceLoader<Translating> translatingServices = ServiceLoader.load(Translating.class);
2    translatingServices.stream()...

and the stream will contain both registered services (Spanish and English).

You can find more information about the module meta file notation at https://www.oracle.com/uk/corporate/features/understanding-java-9-modules.html.

Customised runtime image

Once I played with the Java modules I was tempted to create my own distribution of the Java runtime, which would only contain the modules of my application and the Java modules used by my application.

This is achieved with jlink and an example is visible here:

 1jlink \
 2    --output ./out/slim-runtime \
 3    --module-path ./out/dist \
 4    --add-modules abilities \
 5    --add-modules external.body.parts  \
 6    --add-modules internal.organs \
 7    --add-modules knowledge.language.english \
 8    --add-modules knowledge.language.spanish \
 9    --add-modules human \
10    --launcher speak_human=human

For comparison, the Amazon Corretto distribution is over 300MB but customised distribution for this application takes only about 67MB. Granted, the application is trivial, but it offers some savings.


Other posts in java-features series