Java 9. Project Jigsaw. Модульность
от aNNiMON
Ключевой особенностью предстоящего релиза Java 9 является поддержка модульности, которую принесёт Project Jigsaw. Цель этого проекта — сделать Java SE платформу более гибкой, производительной и защищённой за счёт разбиения JDK на модули и внедрения модульной системы.
Модуль
В отличие от обычного jar-файла, который означал для JVM лишь хранилище кода и ресурсов, jar-модуль содержит класс module-info, который предоставляет:
- имя модуля;
- информацию о модулях-зависимостях, которые нужны для корректной компиляции и работы;
- информацию о пакетах, которые открывает (экспортирует) этот модуль;
- список сервисов, которые поставляет модуль в рантайме.
В третьем пункте кроется одно важное изменение, которого доселе так не хватало. Теперь, если класс объявлен публичным, это ещё не значит, что он будет доступен всем модулям. Область видимости public становится более широкой:
- public class в экспортируемом пакете — доступен всем модулям, у которых этот модуль в зависимостях;
- public class в экспортируемом конкретному модулю пакете — доступен только указанному модулю;
- public class без экспорта пакета — доступен всем классам данного модуля.
Вот пример содержимого module-info.java:
Данный модуль имеет имя com.example.samplemodule (принято именовать модули по пакетам, чтобы избежать конфликтов). Он зависит от модулей com.example.sampleapp, java.httpclient и java.base (который используется по умолчанию для всех модулей). Причём java.httpclient будет зависимостью для всех модулей, которые используют com.example.samplemodule. Наш модуль экспортирует пакеты com.example.samplemodule.model и com.example.samplemodule.spi, так что все публичные классы в этих пакетах будут доступны другим модулям, которые зависят от него. Модуль использует com.example.samplemodule.spi.DataProvider для получения данных из других модулей. А также он поставляет настройки сервису другого модуля, реализуя интерфейс com.example.sampleapp.spi.SettingsProvider в классе com.example.samplemodule.ModuleSettingsProvider.
А теперь к практике.
Пример 1. Прямая зависимость двух модулей
У нас будет два проекта: TimeApp — главное приложение, которое выводит время, поставляемое вторым проектом — TimeLocalModule.
В этом примере TimeApp в главном классе будет вызывать метод публичного класса второго модуля напрямую. Чтобы это работало, мы должны в главном модуле объявить зависимость от второго модуля, а во втором модуле экспортировать пакет с классом, предоставляющим строку текущего времени. Иначе мы даже не сможем импортировать пакет второго модуля.
TimeApp
TimeLocalModule
В настройках главного проекта нужно добавить зависимость от проекта TimeLocalModule:
Netbeans показывает область видимости пакета, com.example.timelocal теперь публичный:
Для компиляции и запуска из командной строки можно воспользоваться скриптом:
Либо запускаем в NetBeans IDE и получаем:
Пример на GitHub
Пример 2. Сервисы и ServiceLoader
Предыдущий пример лишь демонстрирует новый способ подключения библиотеки, теперь же сделаем настоящий модуль. Модифицируем наш пример так, чтобы не главное приложение было зависимо от модулей, а наоборот — модули зависели от главного приложения. На этапе компиляции оно не будет ничего знать о модулях, расширяющих функционал, а вот в рантайме оно будет брать доступные расширения.
Ранее, доступные реализации лежали в текстовом файле в META-INF/services/exampleservice, ServiceLoader читал оттуда названия классов и поставлял в итераторе готовые реализации интерфейсов или абстрактных классов. Теперь же можно назначать реализацию в module-info дополнительных модулей:
А для использования в основном модуле нужно указать
И обрабатывать полученные реализации:
В отличие от старого подхода, связь теперь проверяется на этапе компиляции.
Создадим отдельный пакет com.example.timeapp.spi, который будет публичным для модулей-зависимостей. В нём будет интерфейс, реализуя который, другие модули будут предоставлять информацию.
TimeApp
Не забудьте убрать зависимость от модуля в свойствах проекта, иначе получите циклическую зависимость.
TimeLocalModule
Осталось только указать главному приложению, чтобы он загружал модули, которые в рантайме поставляют реализацию интерфейса TimeProvider. Для этого есть специальная система загрузки сервисов — класс ServiceLoader. Передав ему класс интерфейса провайдера и зарегистрировав этот класс в module-info ключевым словом uses, мы можем получить список реализаций, который доступны в рантайме.
TimeApp
Если сейчас запустить приложение, то мы получим ошибку:
Это потому, что мы не зарегистрировали класс в module-info. Сделаем это:
Теперь при запуске не будет ошибки, но и данные не появятся. Всё потому, что модули не добавлены в зависимости.
В NetBeans IDE в свойствах проекта на вкладке Run нужно добавить модуль в Modulepath, однако добавлять не проект, а jar-файл (предварительно нужно скомпилировать модуль — Clean and Build).
Запускаем и получаем вывод:
Можно добавить ещё один модуль, который будет брать информацию из другого источника, например из Интернета.
TimeNetworkModule
У нового модуля добавлена зависимость java.httpclient для работы с новым HTTP/2 клиентом.
Компилируем и запускаем в NetBeans IDE или из командной строки:
Получаем:
Пример на GitHub
Пример 3. Модули и ресурсы
Исходя из требований модульной системы, ресурсы модуля должны быть доступны только этому модулю. Значит загружать их мы можем только в модуле, а на дальнейшую передачу другим модулям запретов нет.
Давайте сделаем простую форму, где для каждого модуля будет отдельная кнопка, по нажатию которой будем выводить время.
В главном приложении нужно добавить зависимость от java.desktop, чтобы иметь возможность импортировать пакет java.awt и java.swing.
Изменим и TimeProvider, чтобы иметь возможность получить иконку модуля:
Остальным модулям добавим картинки и реализуем метод Image icon() изменённого TimeProvider. Вот только есть одно но. Модули пока ещё не зависят от java.desktop, поэтому класс Image мы не сможем импортировать пока не добавим зависимость в module-info. Как быть? Для этого в module-info есть специальный синтаксис, который позволит указать, что зависимость должна автоматически распространяться и на другие модули:
Тогда module-info главного приложения будет выглядеть так:
Загружаем изображение:
Так же и во втором модуле. Запускаем — работает!
Пример на GitHub
Пример 4. Автоматический модуль
Хорошо, а как быть с библиотеками ранних версий Java? Мы не можем добавить им module-info да и желания нет что-то делать. Как быть? У нас есть два выбора:
1. Добавить библиотеку в modulepath, тогда она становится автоматическим модулем: его именем будет имя jar-файла, экспортировать он будет все свои пакеты, а читать все открытые пакеты других модулей.
2. Добавить библиотеку в classpath, тогда она становится безымянным модулем: добавлять как зависимость мы его, разумеется, не сможем, а читать он сможет все пакеты модулей.
Для примера создадим обычную библиотеку без module-info, реализуем TimeProvider (не забудьте добавить главный проект в classpath):
Скомпилируем, положим jar в отдельную папку, назвав его midnight.jar, и добавим в modulepath:
Теперь мы можем добавлять автоматический модуль midnight:
В ServiceLoader MidnightProvider не попадёт, зато мы можем создать экземпляр класса напрямую. Немного перепишем код в Main, чтобы можно было объединить провайдеры из ServiceLoader с провайдерами, инстанцируемыми прямо в коде:
Пример на GitHub
Вот и всё. Надеюсь у меня получилось приоткрыть завесу тайны Project Jigsaw. За вопросами добро пожаловать в комментарии.
Проект на GitHub: https://github.com/annimon-tutorials/Java-9-Jigsaw-Example
Почитать:
Project Jigsaw: Quick Start Guide
The State of the Module System
Java Platform Module System: Requirements
First steps with Java 9 and Project Jigsaw - Part 1 (перевод)
First steps with Java 9 and Project Jigsaw - Part 2 (перевод)
Следующая статья →Модуль
В отличие от обычного jar-файла, который означал для JVM лишь хранилище кода и ресурсов, jar-модуль содержит класс module-info, который предоставляет:
- имя модуля;
- информацию о модулях-зависимостях, которые нужны для корректной компиляции и работы;
- информацию о пакетах, которые открывает (экспортирует) этот модуль;
- список сервисов, которые поставляет модуль в рантайме.
В третьем пункте кроется одно важное изменение, которого доселе так не хватало. Теперь, если класс объявлен публичным, это ещё не значит, что он будет доступен всем модулям. Область видимости public становится более широкой:
- public class в экспортируемом пакете — доступен всем модулям, у которых этот модуль в зависимостях;
- public class в экспортируемом конкретному модулю пакете — доступен только указанному модулю;
- public class без экспорта пакета — доступен всем классам данного модуля.
Вот пример содержимого module-info.java:
- module com.example.samplemodule {
- requires com.example.sampleapp;
- requires public java.httpclient;
- exports com.example.samplemodule.model;
- exports com.example.samplemodule.spi;
- uses com.example.samplemodule.spi.DataProvider;
- provides com.example.sampleapp.spi.SettingsProvider
- with com.example.samplemodule.ModuleSettingsProvider;
- }
Данный модуль имеет имя com.example.samplemodule (принято именовать модули по пакетам, чтобы избежать конфликтов). Он зависит от модулей com.example.sampleapp, java.httpclient и java.base (который используется по умолчанию для всех модулей). Причём java.httpclient будет зависимостью для всех модулей, которые используют com.example.samplemodule. Наш модуль экспортирует пакеты com.example.samplemodule.model и com.example.samplemodule.spi, так что все публичные классы в этих пакетах будут доступны другим модулям, которые зависят от него. Модуль использует com.example.samplemodule.spi.DataProvider для получения данных из других модулей. А также он поставляет настройки сервису другого модуля, реализуя интерфейс com.example.sampleapp.spi.SettingsProvider в классе com.example.samplemodule.ModuleSettingsProvider.
А теперь к практике.
Пример 1. Прямая зависимость двух модулей
У нас будет два проекта: TimeApp — главное приложение, которое выводит время, поставляемое вторым проектом — TimeLocalModule.
В этом примере TimeApp в главном классе будет вызывать метод публичного класса второго модуля напрямую. Чтобы это работало, мы должны в главном модуле объявить зависимость от второго модуля, а во втором модуле экспортировать пакет с классом, предоставляющим строку текущего времени. Иначе мы даже не сможем импортировать пакет второго модуля.
TimeApp
- // com/example/timeapp/Main.java
- package com.example.timeapp;
- import com.example.timelocal.TimeLocal;
- public final class Main {
- public static void main(String[] args) {
- System.out.format("Current time: %s%n", TimeLocal.now());
- }
- }
- // module-info.java
- module com.example.timeapp {
- requires java.base;
- requires com.example.timelocalmodule;
- }
TimeLocalModule
- // com/example/timelocal/TimeLocal.java
- package com.example.timelocal;
- import java.time.LocalDateTime;
- import java.time.format.DateTimeFormatter;
- public class TimeLocal {
- public static String now() {
- return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
- }
- }
- // module-info.java
- module com.example.timelocalmodule {
- exports com.example.timelocal;
- }
В настройках главного проекта нужно добавить зависимость от проекта TimeLocalModule:
Netbeans показывает область видимости пакета, com.example.timelocal теперь публичный:
Для компиляции и запуска из командной строки можно воспользоваться скриптом:
- @echo off
- set JAVA9_HOME=D:\Program Files\Java\jdk-9
- set JAVA9="%JAVA9_HOME%/bin/java"
- set JAVAC9="%JAVA9_HOME%/bin/javac"
- mkdir mods\com.example.timeapp
- mkdir mods\com.example.timelocalmodule
- echo Compile timelocalmodule
- %JAVAC9% -d mods/com.example.timelocalmodule ^
- TimeLocalModule/src/module-info.java TimeLocalModule/src/com/example/timelocal/TimeLocal.java
- echo Compile timeapp
- %JAVAC9% --module-path mods -d mods/com.example.timeapp ^
- TimeApp/src/module-info.java TimeApp/src/com/example/timeapp/Main.java
- echo Run timeapp
- %JAVA9% --module-path mods ^
- -m com.example.timeapp/com.example.timeapp.Main
Либо запускаем в NetBeans IDE и получаем:
- Current time: 2016-10-20T18:36:36.6763098
Пример на GitHub
Пример 2. Сервисы и ServiceLoader
Предыдущий пример лишь демонстрирует новый способ подключения библиотеки, теперь же сделаем настоящий модуль. Модифицируем наш пример так, чтобы не главное приложение было зависимо от модулей, а наоборот — модули зависели от главного приложения. На этапе компиляции оно не будет ничего знать о модулях, расширяющих функционал, а вот в рантайме оно будет брать доступные расширения.
Ранее, доступные реализации лежали в текстовом файле в META-INF/services/exampleservice, ServiceLoader читал оттуда названия классов и поставлял в итераторе готовые реализации интерфейсов или абстрактных классов. Теперь же можно назначать реализацию в module-info дополнительных модулей:
- module additional {
- provides com.example.spi.Provider // базовый интерфейс
- with com.impl.ProviderImpl; // реализация в классе этого модуля
- }
- module main {
- uses com.example.spi.Provider;
- }
- ServiceLoader<Provider> sl = ServiceLoader.load(Provider.class);
- for (Provider p : sl) {
- // ..
- }
В отличие от старого подхода, связь теперь проверяется на этапе компиляции.
Создадим отдельный пакет com.example.timeapp.spi, который будет публичным для модулей-зависимостей. В нём будет интерфейс, реализуя который, другие модули будут предоставлять информацию.
TimeApp
- // com/example/timeapp/spi/TimeProvider.java
- package com.example.timeapp.spi;
- public interface TimeProvider {
- String now();
- }
- // module-info.java
- module com.example.timeapp {
- requires java.base;
- exports com.example.timeapp.spi;
- }
Не забудьте убрать зависимость от модуля в свойствах проекта, иначе получите циклическую зависимость.
TimeLocalModule
- // com/example/timelocal/TimeLocalProvider.java
- package com.example.timelocal;
- import com.example.timeapp.spi.TimeProvider;
- import java.time.LocalDateTime;
- import java.time.format.DateTimeFormatter;
- public class TimeLocalProvider implements TimeProvider {
- @Override
- public String now() {
- return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
- }
- }
- // module-info.java
- module com.example.timelocalmodule {
- requires com.example.timeapp;
- provides com.example.timeapp.spi.TimeProvider
- with com.example.timelocal.TimeLocalProvider;
- }
Осталось только указать главному приложению, чтобы он загружал модули, которые в рантайме поставляют реализацию интерфейса TimeProvider. Для этого есть специальная система загрузки сервисов — класс ServiceLoader. Передав ему класс интерфейса провайдера и зарегистрировав этот класс в module-info ключевым словом uses, мы можем получить список реализаций, который доступны в рантайме.
TimeApp
- // com/example/timeapp/Main.java
- package com.example.timeapp;
- import com.example.timeapp.spi.TimeProvider;
- import java.util.ServiceLoader;
- public final class Main {
- public static void main(String[] args) {
- ServiceLoader<TimeProvider> serviceLoader = ServiceLoader.load(TimeProvider.class);
- serviceLoader.forEach(t -> {
- System.out.format("Current time: %s%n", t.now());
- System.out.println(t.getClass());
- });
- }
- }
Если сейчас запустить приложение, то мы получим ошибку:
- Exception in thread "main" java.util.ServiceConfigurationError: com.example.timeapp.spi.TimeProvider: use not declared in module com.example.timeapp
- at java.util.ServiceLoader.fail(java.base@9-ea/ServiceLoader.java:386)
- at java.util.ServiceLoader.checkModule(java.base@9-ea/ServiceLoader.java:371)
- at java.util.ServiceLoader.<init>(java.base@9-ea/ServiceLoader.java:319)
- at java.util.ServiceLoader.<init>(java.base@9-ea/ServiceLoader.java:351)
- at java.util.ServiceLoader.load(java.base@9-ea/ServiceLoader.java:1021)
- at com.example.timeapp.Main.main(com.example.timeapp/Main.java:9)
- // module-info.java
- module com.example.timeapp {
- requires java.base;
- exports com.example.timeapp.spi;
- uses com.example.timeapp.spi.TimeProvider;
- }
Теперь при запуске не будет ошибки, но и данные не появятся. Всё потому, что модули не добавлены в зависимости.
В NetBeans IDE в свойствах проекта на вкладке Run нужно добавить модуль в Modulepath, однако добавлять не проект, а jar-файл (предварительно нужно скомпилировать модуль — Clean and Build).
Запускаем и получаем вывод:
- Current time: 2016-10-20T20:41:39.9614732
- class com.example.timelocal.TimeLocalProvider
Можно добавить ещё один модуль, который будет брать информацию из другого источника, например из Интернета.
TimeNetworkModule
- // com/example/timenetwork/TimeNetworkProvider.java
- package com.example.timenetwork;
- import com.example.timeapp.spi.TimeProvider;
- import java.io.IOException;
- import java.net.URI;
- import java.net.http.HttpClient;
- import java.net.http.HttpResponse;
- public class TimeNetworkProvider implements TimeProvider {
- @Override
- public String now() {
- try {
- return HttpClient.getDefault()
- .request(URI.create("http://www.timeapi.org/utc/now"))
- .header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36")
- .GET()
- .response()
- .body(HttpResponse.asString());
- } catch (IOException | InterruptedException ex) {
- throw new RuntimeException("Network error");
- }
- }
- }
- // module-info.java
- module com.example.timenetworkmodule {
- requires com.example.timeapp;
- requires java.httpclient;
- provides com.example.timeapp.spi.TimeProvider
- with com.example.timenetwork.TimeNetworkProvider;
- }
У нового модуля добавлена зависимость java.httpclient для работы с новым HTTP/2 клиентом.
Компилируем и запускаем в NetBeans IDE или из командной строки:
- rem compile.cmd
- echo Compile timeapp
- %JAVAC9% -d mods/com.example.timeapp ^
- TimeApp/src/module-info.java TimeApp/src/com/example/timeapp/Main.java ^
- TimeApp/src/com/example/timeapp/spi/TimeProvider.java
- echo Compile timelocalmodule
- %JAVAC9% --module-path mods -d mods/com.example.timelocalmodule ^
- TimeLocalModule/src/module-info.java TimeLocalModule/src/com/example/timelocal/TimeLocal.java ^
- TimeLocalModule/src/com/example/timelocal/TimeLocalProvider.java
- echo Compile timenetworkmodule
- %JAVAC9% --module-path mods -d mods/com.example.timenetworkmodule ^
- TimeNetworkModule/src/module-info.java ^
- TimeNetworkModule/src/com/example/timenetwork/TimeNetworkProvider.java
- echo Run timeapp
- %JAVA9% --module-path mods ^
- -m com.example.timeapp/com.example.timeapp.Main
Получаем:
- Current time: 2016-10-20T20:55:03.3944269
- class com.example.timelocal.TimeLocalProvider
- Current time: 2016-10-20T17:55:06+00:00
- class com.example.timenetwork.TimeNetworkProvider
Пример на GitHub
Пример 3. Модули и ресурсы
Исходя из требований модульной системы, ресурсы модуля должны быть доступны только этому модулю. Значит загружать их мы можем только в модуле, а на дальнейшую передачу другим модулям запретов нет.
Давайте сделаем простую форму, где для каждого модуля будет отдельная кнопка, по нажатию которой будем выводить время.
В главном приложении нужно добавить зависимость от java.desktop, чтобы иметь возможность импортировать пакет java.awt и java.swing.
- // module-info.java
- module com.example.timeapp {
- requires java.base;
- requires java.desktop;
- exports com.example.timeapp.spi;
- uses com.example.timeapp.spi.TimeProvider;
- }
Изменим и TimeProvider, чтобы иметь возможность получить иконку модуля:
- // com/example/timeapp/spi/TimeProvider.java
- package com.example.timeapp.spi;
- import java.awt.Image;
- public interface TimeProvider {
- String now();
- Image icon();
- }
- // com/example/timeapp/Main.java
- public final class Main extends JFrame {
- public static void main(String[] args) {
- final Main frame = new Main();
- ServiceLoader<TimeProvider> serviceLoader = ServiceLoader.load(TimeProvider.class);
- serviceLoader.forEach(t -> {
- final JButton button = new JButton();
- button.setText(t.getClass().getSimpleName());
- final Image icon = t.icon();
- if (icon != null) {
- button.setIcon(new ImageIcon(icon));
- }
- button.addActionListener(e -> {
- frame.outputLabel.setText(String.format("Current time: %s%n", t.now()));
- });
- frame.modulesPanel.add(button);
- });
- frame.pack();
- frame.setVisible(true);
- }
- private final JPanel modulesPanel;
- private final JLabel outputLabel;
- public Main() {
- super("Jigsaw Example");
- modulesPanel = new JPanel();
- modulesPanel.setLayout(new BoxLayout(modulesPanel, BoxLayout.LINE_AXIS));
- add(modulesPanel, BorderLayout.NORTH);
- outputLabel = new JLabel("output");
- outputLabel.setHorizontalAlignment(SwingConstants.CENTER);
- add(outputLabel, BorderLayout.CENTER);
- setDefaultCloseOperation(EXIT_ON_CLOSE);
- }
- }
Остальным модулям добавим картинки и реализуем метод Image icon() изменённого TimeProvider. Вот только есть одно но. Модули пока ещё не зависят от java.desktop, поэтому класс Image мы не сможем импортировать пока не добавим зависимость в module-info. Как быть? Для этого в module-info есть специальный синтаксис, который позволит указать, что зависимость должна автоматически распространяться и на другие модули:
- requires public java.desktop;
Тогда module-info главного приложения будет выглядеть так:
- // module-info.java
- module com.example.timeapp {
- requires java.base;
- requires public java.desktop;
- exports com.example.timeapp.spi;
- uses com.example.timeapp.spi.TimeProvider;
- }
Загружаем изображение:
- public class TimeLocalProvider implements TimeProvider {
- private static Image icon;
- @Override
- public String now() {
- return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
- }
- @Override
- public Image icon() {
- if (icon == null) {
- try (InputStream is = getClass().getResourceAsStream("/res/icon.png")) {
- if (is != null)
- icon = ImageIO.read(is);
- } catch (IOException ignore) { }
- }
- return icon;
- }
- }
Так же и во втором модуле. Запускаем — работает!
Пример на GitHub
Пример 4. Автоматический модуль
Хорошо, а как быть с библиотеками ранних версий Java? Мы не можем добавить им module-info да и желания нет что-то делать. Как быть? У нас есть два выбора:
1. Добавить библиотеку в modulepath, тогда она становится автоматическим модулем: его именем будет имя jar-файла, экспортировать он будет все свои пакеты, а читать все открытые пакеты других модулей.
2. Добавить библиотеку в classpath, тогда она становится безымянным модулем: добавлять как зависимость мы его, разумеется, не сможем, а читать он сможет все пакеты модулей.
Для примера создадим обычную библиотеку без module-info, реализуем TimeProvider (не забудьте добавить главный проект в classpath):
- // com/example/timemidnight/MidnightProvider.java
- package com.example.timemidnight;
- import com.example.timeapp.spi.TimeProvider;
- import java.awt.Image;
- import java.io.IOException;
- import java.io.InputStream;
- import javax.imageio.ImageIO;
- public class MidnightProvider implements TimeProvider {
- private static Image icon;
- @Override
- public String now() {
- return "00:00";
- }
- @Override
- public Image icon() {
- if (icon == null) {
- try (InputStream is = getClass().getResourceAsStream("/res/icon.png")) {
- if (is != null)
- icon = ImageIO.read(is);
- } catch (IOException ignore) { }
- }
- return icon;
- }
- }
Скомпилируем, положим jar в отдельную папку, назвав его midnight.jar, и добавим в modulepath:
Теперь мы можем добавлять автоматический модуль midnight:
- // module-info
- module com.example.timeapp {
- requires java.base;
- requires public java.desktop;
- requires midnight;
- exports com.example.timeapp.spi;
- uses com.example.timeapp.spi.TimeProvider;
- }
В ServiceLoader MidnightProvider не попадёт, зато мы можем создать экземпляр класса напрямую. Немного перепишем код в Main, чтобы можно было объединить провайдеры из ServiceLoader с провайдерами, инстанцируемыми прямо в коде:
- Stream.concat(
- StreamSupport.stream(serviceLoader.spliterator(), false),
- Stream.of(new MidnightProvider())) // we can directly access to class from automatic module
- .forEach(t -> ...
Пример на GitHub
Вот и всё. Надеюсь у меня получилось приоткрыть завесу тайны Project Jigsaw. За вопросами добро пожаловать в комментарии.
Проект на GitHub: https://github.com/annimon-tutorials/Java-9-Jigsaw-Example
Почитать:
Project Jigsaw: Quick Start Guide
The State of the Module System
Java Platform Module System: Requirements
First steps with Java 9 and Project Jigsaw - Part 1 (перевод)
First steps with Java 9 and Project Jigsaw - Part 2 (перевод)
java.awt.Taskbar