Java 9. Project Jigsaw. Модульность

от
Java    java 9, early access, jigsaw, modularity, модульность

Ключевой особенностью предстоящего релиза Java 9 является поддержка модульности, которую принесёт Project Jigsaw. Цель этого проекта — сделать Java SE платформу более гибкой, производительной и защищённой за счёт разбиения JDK на модули и внедрения модульной системы.

Модуль
В отличие от обычного jar-файла, который означал для JVM лишь хранилище кода и ресурсов, jar-модуль содержит класс module-info, который предоставляет:
  - имя модуля;
  - информацию о модулях-зависимостях, которые нужны для корректной компиляции и работы;
  - информацию о пакетах, которые открывает (экспортирует) этот модуль;
  - список сервисов, которые поставляет модуль в рантайме.

В третьем пункте кроется одно важное изменение, которого доселе так не хватало. Теперь, если класс объявлен публичным, это ещё не значит, что он будет доступен всем модулям. Область видимости public становится более широкой:
  - public class в экспортируемом пакете — доступен всем модулям, у которых этот модуль в зависимостях;
  - public class в экспортируемом конкретному модулю пакете — доступен только указанному модулю;
  - public class без экспорта пакета — доступен всем классам данного модуля.

Вот пример содержимого module-info.java:
  1. module com.example.samplemodule {
  2.     requires com.example.sampleapp;
  3.     requires public java.httpclient;
  4.     exports com.example.samplemodule.model;
  5.     exports com.example.samplemodule.spi;
  6.     uses com.example.samplemodule.spi.DataProvider;
  7.     provides com.example.sampleapp.spi.SettingsProvider
  8.         with com.example.samplemodule.ModuleSettingsProvider;
  9. }

Данный модуль имеет имя 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
  1. // com/example/timeapp/Main.java
  2. package com.example.timeapp;
  3.  
  4. import com.example.timelocal.TimeLocal;
  5.  
  6. public final class Main {
  7.  
  8.     public static void main(String[] args) {
  9.         System.out.format("Current time: %s%n", TimeLocal.now());
  10.     }
  11. }
  1. // module-info.java
  2. module com.example.timeapp {
  3.     requires java.base;
  4.     requires com.example.timelocalmodule;
  5. }

TimeLocalModule
  1. // com/example/timelocal/TimeLocal.java
  2. package com.example.timelocal;
  3.  
  4. import java.time.LocalDateTime;
  5. import java.time.format.DateTimeFormatter;
  6.  
  7. public class TimeLocal {
  8.  
  9.     public static String now() {
  10.         return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
  11.     }
  12. }
  1. // module-info.java
  2. module com.example.timelocalmodule {
  3.     exports com.example.timelocal;
  4. }

В настройках главного проекта нужно добавить зависимость от проекта TimeLocalModule:
Добавление зависимости

Добавление зависимости

Netbeans показывает область видимости пакета, com.example.timelocal теперь публичный:
Область видимости пакета в Netbeans IDE

Для компиляции и запуска из командной строки можно воспользоваться скриптом:
  1. @echo off
  2.  
  3. set JAVA9_HOME=D:\Program Files\Java\jdk-9
  4. set JAVA9="%JAVA9_HOME%/bin/java"
  5. set JAVAC9="%JAVA9_HOME%/bin/javac"
  6.  
  7. mkdir mods\com.example.timeapp
  8. mkdir mods\com.example.timelocalmodule
  9.  
  10. echo Compile timelocalmodule
  11. %JAVAC9% -d mods/com.example.timelocalmodule ^
  12.     TimeLocalModule/src/module-info.java TimeLocalModule/src/com/example/timelocal/TimeLocal.java
  13.  
  14. echo Compile timeapp
  15. %JAVAC9% --module-path mods -d mods/com.example.timeapp ^
  16.     TimeApp/src/module-info.java TimeApp/src/com/example/timeapp/Main.java
  17.  
  18. echo Run timeapp
  19. %JAVA9% --module-path mods ^
  20.         -m com.example.timeapp/com.example.timeapp.Main

Либо запускаем в NetBeans IDE и получаем:
  1. Current time: 2016-10-20T18:36:36.6763098

Пример на GitHub


Пример 2. Сервисы и ServiceLoader
Предыдущий пример лишь демонстрирует новый способ подключения библиотеки, теперь же сделаем настоящий модуль. Модифицируем наш пример так, чтобы не главное приложение было зависимо от модулей, а наоборот — модули зависели от главного приложения. На этапе компиляции оно не будет ничего знать о модулях, расширяющих функционал, а вот в рантайме оно будет брать доступные расширения.

Ранее, доступные реализации лежали в текстовом файле в META-INF/services/exampleservice, ServiceLoader читал оттуда названия классов и поставлял в итераторе готовые реализации интерфейсов или абстрактных классов. Теперь же можно назначать реализацию в module-info дополнительных модулей:
  1. module additional {
  2.   provides com.example.spi.Provider // базовый интерфейс
  3.       with com.impl.ProviderImpl; // реализация в классе этого модуля
  4. }
А для использования в основном модуле нужно указать
  1. module main {
  2.   uses com.example.spi.Provider;
  3. }
И обрабатывать полученные реализации:
  1. ServiceLoader<Provider> sl = ServiceLoader.load(Provider.class);
  2. for (Provider p : sl) {
  3.   // ..
  4. }

В отличие от старого подхода, связь теперь проверяется на этапе компиляции.


Создадим отдельный пакет com.example.timeapp.spi, который будет публичным для модулей-зависимостей. В нём будет интерфейс, реализуя который, другие модули будут предоставлять информацию.

TimeApp
  1. // com/example/timeapp/spi/TimeProvider.java
  2. package com.example.timeapp.spi;
  3.  
  4. public interface TimeProvider {
  5.  
  6.     String now();
  7. }
  1. // module-info.java
  2. module com.example.timeapp {
  3.     requires java.base;
  4.  
  5.     exports com.example.timeapp.spi;
  6. }

Не забудьте убрать зависимость от модуля в свойствах проекта, иначе получите циклическую зависимость.


TimeLocalModule
  1. // com/example/timelocal/TimeLocalProvider.java
  2. package com.example.timelocal;
  3.  
  4. import com.example.timeapp.spi.TimeProvider;
  5. import java.time.LocalDateTime;
  6. import java.time.format.DateTimeFormatter;
  7.  
  8. public class TimeLocalProvider implements TimeProvider {
  9.  
  10.     @Override
  11.     public String now() {
  12.         return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
  13.     }
  14. }
  1. // module-info.java
  2. module com.example.timelocalmodule {
  3.     requires com.example.timeapp;
  4.  
  5.     provides com.example.timeapp.spi.TimeProvider
  6.         with com.example.timelocal.TimeLocalProvider;
  7. }

Осталось только указать главному приложению, чтобы он загружал модули, которые в рантайме поставляют реализацию интерфейса TimeProvider. Для этого есть специальная система загрузки сервисов — класс ServiceLoader. Передав ему класс интерфейса провайдера и зарегистрировав этот класс в module-info ключевым словом uses, мы можем получить список реализаций, который доступны в рантайме.

TimeApp
  1. // com/example/timeapp/Main.java
  2. package com.example.timeapp;
  3.  
  4. import com.example.timeapp.spi.TimeProvider;
  5. import java.util.ServiceLoader;
  6.  
  7. public final class Main {
  8.  
  9.     public static void main(String[] args) {
  10.         ServiceLoader<TimeProvider> serviceLoader = ServiceLoader.load(TimeProvider.class);
  11.         serviceLoader.forEach(t -> {
  12.             System.out.format("Current time: %s%n", t.now());
  13.             System.out.println(t.getClass());
  14.         });
  15.     }
  16. }

Если сейчас запустить приложение, то мы получим ошибку:
  1. Exception in thread "main" java.util.ServiceConfigurationError: com.example.timeapp.spi.TimeProvider: use not declared in module com.example.timeapp
  2.     at java.util.ServiceLoader.fail(java.base@9-ea/ServiceLoader.java:386)
  3.     at java.util.ServiceLoader.checkModule(java.base@9-ea/ServiceLoader.java:371)
  4.     at java.util.ServiceLoader.<init>(java.base@9-ea/ServiceLoader.java:319)
  5.     at java.util.ServiceLoader.<init>(java.base@9-ea/ServiceLoader.java:351)
  6.     at java.util.ServiceLoader.load(java.base@9-ea/ServiceLoader.java:1021)
  7.     at com.example.timeapp.Main.main(com.example.timeapp/Main.java:9)
Это потому, что мы не зарегистрировали класс в module-info. Сделаем это:
  1. // module-info.java
  2. module com.example.timeapp {
  3.     requires java.base;
  4.  
  5.     exports com.example.timeapp.spi;
  6.  
  7.     uses com.example.timeapp.spi.TimeProvider;
  8. }

Теперь при запуске не будет ошибки, но и данные не появятся. Всё потому, что модули не добавлены в зависимости.

В NetBeans IDE в свойствах проекта на вкладке Run нужно добавить модуль в Modulepath, однако добавлять не проект, а jar-файл (предварительно нужно скомпилировать модуль — Clean and Build).

Добавление модуля

Запускаем и получаем вывод:
  1. Current time: 2016-10-20T20:41:39.9614732
  2. class com.example.timelocal.TimeLocalProvider


Можно добавить ещё один модуль, который будет брать информацию из другого источника, например из Интернета.

TimeNetworkModule
  1. // com/example/timenetwork/TimeNetworkProvider.java
  2. package com.example.timenetwork;
  3.  
  4. import com.example.timeapp.spi.TimeProvider;
  5. import java.io.IOException;
  6. import java.net.URI;
  7. import java.net.http.HttpClient;
  8. import java.net.http.HttpResponse;
  9.  
  10. public class TimeNetworkProvider implements TimeProvider {
  11.  
  12.     @Override
  13.     public String now() {
  14.         try {
  15.             return HttpClient.getDefault()
  16.                     .request(URI.create("http://www.timeapi.org/utc/now"))
  17.                     .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")
  18.                     .GET()
  19.                     .response()
  20.                     .body(HttpResponse.asString());
  21.         } catch (IOException | InterruptedException ex) {
  22.             throw new RuntimeException("Network error");
  23.         }
  24.     }
  25. }
  1. // module-info.java
  2. module com.example.timenetworkmodule {
  3.     requires com.example.timeapp;
  4.     requires java.httpclient;
  5.  
  6.     provides com.example.timeapp.spi.TimeProvider
  7.             with com.example.timenetwork.TimeNetworkProvider;
  8. }

У нового модуля добавлена зависимость java.httpclient для работы с новым HTTP/2 клиентом.

Компилируем и запускаем в NetBeans IDE или из командной строки:
  1. rem compile.cmd
  2. echo Compile timeapp
  3. %JAVAC9% -d mods/com.example.timeapp ^
  4.     TimeApp/src/module-info.java TimeApp/src/com/example/timeapp/Main.java ^
  5.     TimeApp/src/com/example/timeapp/spi/TimeProvider.java
  6.  
  7. echo Compile timelocalmodule
  8. %JAVAC9% --module-path mods -d mods/com.example.timelocalmodule ^
  9.     TimeLocalModule/src/module-info.java TimeLocalModule/src/com/example/timelocal/TimeLocal.java ^
  10.     TimeLocalModule/src/com/example/timelocal/TimeLocalProvider.java
  11.  
  12. echo Compile timenetworkmodule
  13. %JAVAC9% --module-path mods -d mods/com.example.timenetworkmodule ^
  14.     TimeNetworkModule/src/module-info.java ^
  15.     TimeNetworkModule/src/com/example/timenetwork/TimeNetworkProvider.java
  16.  
  17. echo Run timeapp
  18. %JAVA9% --module-path mods ^
  19.         -m com.example.timeapp/com.example.timeapp.Main

Получаем:
  1. Current time: 2016-10-20T20:55:03.3944269
  2. class com.example.timelocal.TimeLocalProvider
  3. Current time: 2016-10-20T17:55:06+00:00
  4. class com.example.timenetwork.TimeNetworkProvider

Пример на GitHub


Пример 3. Модули и ресурсы
Исходя из требований модульной системы, ресурсы модуля должны быть доступны только этому модулю. Значит загружать их мы можем только в модуле, а на дальнейшую передачу другим модулям запретов нет.

Давайте сделаем простую форму, где для каждого модуля будет отдельная кнопка, по нажатию которой будем выводить время.

В главном приложении нужно добавить зависимость от java.desktop, чтобы иметь возможность импортировать пакет java.awt и java.swing.

  1. // module-info.java
  2. module com.example.timeapp {
  3.     requires java.base;
  4.     requires java.desktop;
  5.  
  6.     exports com.example.timeapp.spi;
  7.  
  8.     uses com.example.timeapp.spi.TimeProvider;
  9. }

Изменим и TimeProvider, чтобы иметь возможность получить иконку модуля:
  1. // com/example/timeapp/spi/TimeProvider.java
  2. package com.example.timeapp.spi;
  3.  
  4. import java.awt.Image;
  5.  
  6. public interface TimeProvider {
  7.  
  8.     String now();
  9.  
  10.     Image icon();
  11. }

  1. // com/example/timeapp/Main.java
  2. public final class Main extends JFrame {
  3.  
  4.     public static void main(String[] args) {
  5.         final Main frame = new Main();
  6.         ServiceLoader<TimeProvider> serviceLoader = ServiceLoader.load(TimeProvider.class);
  7.         serviceLoader.forEach(t -> {
  8.             final JButton button = new JButton();
  9.             button.setText(t.getClass().getSimpleName());
  10.             final Image icon = t.icon();
  11.             if (icon != null) {
  12.                 button.setIcon(new ImageIcon(icon));
  13.             }
  14.             button.addActionListener(e -> {
  15.                 frame.outputLabel.setText(String.format("Current time: %s%n", t.now()));
  16.             });
  17.             frame.modulesPanel.add(button);
  18.         });
  19.         frame.pack();
  20.         frame.setVisible(true);
  21.     }
  22.  
  23.     private final JPanel modulesPanel;
  24.     private final JLabel outputLabel;
  25.  
  26.     public Main() {
  27.         super("Jigsaw Example");
  28.  
  29.         modulesPanel = new JPanel();
  30.         modulesPanel.setLayout(new BoxLayout(modulesPanel, BoxLayout.LINE_AXIS));
  31.         add(modulesPanel, BorderLayout.NORTH);
  32.  
  33.         outputLabel = new JLabel("output");
  34.         outputLabel.setHorizontalAlignment(SwingConstants.CENTER);
  35.         add(outputLabel, BorderLayout.CENTER);
  36.  
  37.         setDefaultCloseOperation(EXIT_ON_CLOSE);
  38.     }
  39. }

Остальным модулям добавим картинки и реализуем метод Image icon() изменённого TimeProvider. Вот только есть одно но. Модули пока ещё не зависят от java.desktop, поэтому класс Image мы не сможем импортировать пока не добавим зависимость в module-info. Как быть? Для этого в module-info есть специальный синтаксис, который позволит указать, что зависимость должна автоматически распространяться и на другие модули:
  1. requires public java.desktop;

Тогда module-info главного приложения будет выглядеть так:
  1. // module-info.java
  2. module com.example.timeapp {
  3.     requires java.base;
  4.     requires public java.desktop;
  5.  
  6.     exports com.example.timeapp.spi;
  7.  
  8.     uses com.example.timeapp.spi.TimeProvider;
  9. }

Загружаем изображение:
  1. public class TimeLocalProvider implements TimeProvider {
  2.  
  3.     private static Image icon;
  4.  
  5.     @Override
  6.     public String now() {
  7.         return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now());
  8.     }
  9.  
  10.     @Override
  11.     public Image icon() {
  12.         if (icon == null) {
  13.             try (InputStream is = getClass().getResourceAsStream("/res/icon.png")) {
  14.                 if (is != null)
  15.                     icon = ImageIO.read(is);
  16.             } catch (IOException ignore) { }
  17.         }
  18.         return icon;
  19.     }
  20. }

Так же и во втором модуле. Запускаем — работает!

Ресурсы загружены успешно

Пример на GitHub


Пример 4. Автоматический модуль
Хорошо, а как быть с библиотеками ранних версий Java? Мы не можем добавить им module-info да и желания нет что-то делать. Как быть? У нас есть два выбора:
     1. Добавить библиотеку в modulepath, тогда она становится автоматическим модулем: его именем будет имя jar-файла, экспортировать он будет все свои пакеты, а читать все открытые пакеты других модулей.
     2. Добавить библиотеку в classpath, тогда она становится безымянным модулем: добавлять как зависимость мы его, разумеется, не сможем, а читать он сможет все пакеты модулей.

Для примера создадим обычную библиотеку без module-info, реализуем TimeProvider (не забудьте добавить главный проект в classpath):
  1. // com/example/timemidnight/MidnightProvider.java
  2. package com.example.timemidnight;
  3.  
  4. import com.example.timeapp.spi.TimeProvider;
  5. import java.awt.Image;
  6. import java.io.IOException;
  7. import java.io.InputStream;
  8. import javax.imageio.ImageIO;
  9.  
  10. public class MidnightProvider implements TimeProvider {
  11.  
  12.     private static Image icon;
  13.  
  14.     @Override
  15.     public String now() {
  16.         return "00:00";
  17.     }
  18.  
  19.     @Override
  20.     public Image icon() {
  21.         if (icon == null) {
  22.             try (InputStream is = getClass().getResourceAsStream("/res/icon.png")) {
  23.                 if (is != null)
  24.                     icon = ImageIO.read(is);
  25.             } catch (IOException ignore) { }
  26.         }
  27.         return icon;
  28.     }
  29. }

Скомпилируем, положим jar в отдельную папку, назвав его midnight.jar, и добавим в modulepath:

Зависимости в modulepath

Теперь мы можем добавлять автоматический модуль midnight:
  1. // module-info
  2. module com.example.timeapp {
  3.     requires java.base;
  4.     requires public java.desktop;
  5.     requires midnight;
  6.  
  7.     exports com.example.timeapp.spi;
  8.  
  9.     uses com.example.timeapp.spi.TimeProvider;
  10. }

В ServiceLoader MidnightProvider не попадёт, зато мы можем создать экземпляр класса напрямую. Немного перепишем код в Main, чтобы можно было объединить провайдеры из ServiceLoader с провайдерами, инстанцируемыми прямо в коде:
  1. Stream.concat(
  2.         StreamSupport.stream(serviceLoader.spliterator(), false),
  3.         Stream.of(new MidnightProvider())) // we can directly access to class from automatic module
  4.         .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 (перевод)
  • +5
  • views 8895