Основным нововведением Java 9 было именно введение модульности. Про эту фичу было много разговоров, дата релиза несколько раз переносилась, чтобы допилить все должным образом. В этом посте речь пойдет о том, что дает механизм модулей, и чего полезного Java 9 принесла в целом. Основой для поста послужил доклад моего коллеги — Сергея Малькевича.




Для реализации модулей в этой версии Java был выделен целый проект — Project Jigsaw — который включает в себя несколько JEP и JSR.



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

Подробнее о Project Jigsaw


Project Jigsaw, который реализует модульность, начал разрабатываться в далеком 2005: сначала вышел JSR 277, а уже в 2008 началась непосредственная работа над проектом. Релиз состоялся только в 2017 году. То есть, для того, чтобы докрутить модули в Java, понадобилось почти 10 лет. Что, собственно, подчеркивает весь масштаб работы и изменений, которые были внесены в ходе реализации модульности.

Какие цели ставили перед собой разработчики:

  • облегчить разработку больших приложений и библиотек;
  • улучшить безопасность Java SE в целом, и JDK в частности;
  • увеличить производительность приложений;
  • создать возможность уменьшения размера JRE для запуска на небольших девайсах, чтобы не потреблять слишком много памяти;
  • JAR HELL (об этом чуть позже).

Чего полезного принесла Java 9


До 9 версии, JDK и JRE были монолитными. Их размер рос с каждым релизом. Java 8 занимала уже сотни мегабайт, и все это разработчикам приходилось “таскать с собой” каждый раз, чтобы иметь возможность запускать Java приложения. Один только rt.jar весит порядка 60 Mb. Ну и сюда еще добавляем медленный старт и высокое потребление памяти. Тут на помощь пришла Java 9.

В JDK 9 было введено разделение на модули, а именно, JDK была разделена на 73 модуля. И с каждой новой версией количество этих модулей растет. В 11 версии это число близится к 100. Это разделение позволило разработчикам создать утилиту JLINK. С помощью JLINK можно создавать кастомные наборы JRE, которые будут включают только «нужные» модули, которые реально необходимы вашему приложению. Таким образом, простое приложение и какой-либо customJRE с минимальным (или небольшим) набором модулей в итоге может уместиться в 20 Mb, что не может не радовать.

Список модулей можно посмотреть здесь.

С приходом Java 9 поменялась структура JDK: теперь она идентична структуре JRE. Если раньше JDK включала папку JRE, где снова имеется bin и дублируются файлы, то теперь все выглядит следующим образом:



Модули


Собственно. что такое модуль? Модуль — это новый уровень агрегации пакетов и ресурсов (ориг. “a uniquely named, reusable group of related packages, as well as resources and a module descriptor”).

Модули поставляются в JAR файлах с пакетами и дескриптором модуля
module-info.java. Файл module-info.java содержит описание модуля:
имя, зависимости, экспортируемые пакеты, потребляемые и предоставляемые сервисы, разрешения для reflection доступа.

Примеры описания дескриптора модуля:

module java.sql {
    requires transitive java.logging;
    requires transitive java.transaction.xa;
    requires transitive java.xml;

    exports java.sql;
    exports javax.sql;

    uses java.sql.Driver;
}

module jdk.javadoc {
   requires java.xml;
   
   requires transitive java.compiler;
   requires transitive jdk.compiler;
   
   exports jdk.javadoc.doclet;
   
   provides java.util.spi.ToolProvider with
       jdk.javadoc.internal.tool.JavadocToolProvider;
   
   provides javax.tools.DocumentationTool with
       jdk.javadoc.internal.api.JavadocTool;
   
   provides javax.tools.Tool with
      jdk.javadoc.internal.api.JavadocTool;   
}

После ключевого слова module у нас идет имя пакета jdk.javadoc, который зависит от другого пакета java.xml и транзитивно зависит от других пакетов.

Давайте подробнее пройдемся по каждому из ключевых слов:

  • requires указывает модули, от которых зависит текущий модуль;

  • requires transitive — транзитивная зависимость — означает следующее: если модуль m1 транзитивно зависит от модуля m2, и мы имеем какой-то третий модуль mX, который зависит от m1 — модуль mX будет иметь доступ также и к m2;

  • requires static позволяет указать compile-time зависимости;

  • exports указывает пакеты, которые экспортирует текущий модуль (не включая “подпакеты”);

  • exports...to… позволяет ограничить доступ: export com.my.package.name to com.specific.package; то есть можно открыть доступ к пакету нашего модуля только для какого-то другого(их) пакета(ов) другого модуля;

  • uses указывает, какие сервисы использует модуль:

    uses java.sql.Driver;

    В данном случае, мы указываем интерфейс используемого сервиса;

  • provides указывает, какие сервисы предоставляет модуль:

    provides javax.tools.Tool with
        jdk.javadoc.internal.api.JavadocTool;

    Сначала указываем интерфейс — javax.tools.Tool, после with — реализацию.

Немного о сервисах

Допустим, у нас подключено несколько модулей, которые реализуют абстрактный сервис — MyService. При сборке приложения у нас есть возможность решить, какую реализацию сервиса использовать, “перетащив” нужные нам модули реализации сервиса на --module-path:

Iterable<MyService> services = 
        ServiceLoader.load(MyService.class);

Таким образом, возвращенный Iterator содержит список реализаций интерфейса MyService. Фактически, он будет содержать все реализации, найденные в модулях, найденных на --module-path.

Зачем в принципе были введены сервисы? Они нужны для того, чтобы показать, как наш код будет использован. То есть, здесь заключена семантическая роль. Также, модульность — это про инкапсуляцию и безопасность, так как мы можем сделать реализацию private и исключить возможность несанкционированного доступа через reflection.

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

Вернемся к синтаксису описания модулей:

До 9ки через reflection мы имели доступ практически ко всему и могли делать все, что хотим и с чем хотим. А 9-ая версия, как уже упоминалось, позволяет обезопасить себя от “нелегального” reflection доступа.

Мы можем полностью открыть модуль для reflection доступа, объявив open:

open module my.module {
}

Либо, мы можем указать какие либо пакеты для reflection доступа, объявив opens:

module my.module {
    opens com.my.coolpackage;
}

Здесь же есть возможность использовать opens com.my.coolpackage to, таким образом предоставляя reflection доступ пакету com.my.coolpackage из пакета, который укажем после to.

Типы модулей


Project Jigsaw классифицирует модули следующим образом:

  • System Modules — Java SE и JDK модули. Полный список можно посмотреть, используя команду java --list-modules.

  • Application Modules — модули нашего приложения, которые мы написали, а также те зависимости (от сторонних библиотек), которые использует наше приложение.

  • Automatic Modules — это модули с открытым доступом, создаваемые Java автоматически из JAR-файлов. Допустим, мы хотим запустить наше приложение в модульном режиме, но оно использует какую-то библиотеку. В этом случае мы помещаем JAR-файл на --module-path и Java автоматически создает модуль с именем, унаследованным от имени JAR-файла.

  • Unnamed Module — безымянный модуль, автоматически создаваемый из всех JAR-файлов, которые загружены на --class-path. Это универсальный модуль для обеспечения обратной совместимости с ранее написанным Java кодом.

Class-path vs module-path


С появлением модулей появилось новое понятие — module-path. По сути, это тот же class-path, но для модулей.

Запуск модульного приложения выглядит следующим образом:



В обычном режиме запуска мы указываем опции и полный путь к мейн классу. В случае, если мы хотим работать с модулями, мы также указываем опции и параметр -m либо -module, который как раз указывает на то, что мы будем запускать модули. То есть, мы автоматически переводим наше приложение в модульный режим. Далее мы указываем имя модуля и путь к мейн классу из модуля.

Также, если в обычном режиме мы привыкли работать с параметром -cp и --class-path, в режиме модульности мы прописываем новый параметр -p и --module-path, который указывает пути к используемым в приложении модулям.

Часто встречаюсь с тем, что разработчики не переходят на версии 9+, так как считают, что им придется работать с модулями. Хотя на самом деле, мы можем запускать наши приложения в старом режиме, попросту не прописывая параметр и не используя модули, а используя только другие новые фишки.

JAR HELL


Хочу также по диагонали остановится на проблеме Jar Hell.



Что такое Jar Hell в двух словах? Например, у нас есть какое-то наше приложение и оно зависит от библиотеки X и библиотеки Y. При этом, обе эти библиотеки зависят от библиотеки Z, но от разных версий: X зависит от версии 1, Y — от версии 2. Хорошо, если версия 2 обратно совместима с версией 1, тогда никаких проблем не возникнет. А если нет — очевидно, что мы получаем конфликт версий, то есть одна и та же библиотека не может быть загружена в память одним и тем же загрузчиком классов.

Как в этом случае выходят из ситуации? Есть стандартные методы, которые разработчики используют со времен самой первой Java, например, exclude, кто-то использует плагины для Maven, которые переименовывают названия корневых пакетов библиотеки. Либо же, разработчики ищут разные версии библиотеки X, чтобы подобрать совместимый вариант.

К чему это я: первые прототипы Jigsaw подразумевали наличие версии у модуля и позволяли загрузку нескольких версий через разные ClassLoader’ы, но позже от это отказались. В итоге, “серебряной пули”, которую многие ждали, не вышло.

Но, “прямо-из-коробки” нас немного обезопасили от подобных проблем. В Java 9 запрещены Split Packages — пакеты, которые разделены на несколько модулей. То есть, если у нас есть пакет com.my.coolpackage в одном модуле, мы не можем его использовать в другом модуле в рамках одного приложения. При запуске приложения с модулями, содержащими одинаковые пакеты, мы просто упадем. Это небольшое улучшение исключает возможность непредсказуемого поведения в связи с загрузкой Split пакетов.

Также, помимо самих модулей, есть еще механизм слоев или Jigsaw Layers, который также помогает справится с проблемой Jar Hell.

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

Выглядит это следующим образом:



При старте приложения создается слой boot, куда входят модули платформы, загружаемые Bootstrap, добавочные модули платформы, загружаемые платформенным загрузчиком и модули нашего приложения, загружаемые Application загрузчиком.

В любой момент, мы можем создать свои слои и “положить” туда модули разных версий и при этом не упасть.

Есть отличный подробный доклад на YouTube на эту тему: Спасение от Jar Hell с помощью Jigsaw Layers

Заключение


Механизм модулей из Java 9 открывает нам новые возможности, при этом поддержка библиотек на сегодняшний день довольно небольшая. Да, люди запускают Spring, Spring Boot и так далее. Но большинство библиотек так и не перешло на полное использование модулей. Видимо поэтому, все эти изменения были восприняты довольно скептически техничесим сообществом. Модули предоставляют нам новые возможности, но вопрос востребованности остаётся открытым.

Ну и напоследок, предлагаю подборку материалов на эту тему:

Project Jigsaw

JDK Module Summary

Paul Deitel — Understanding Java 9 Modules

baeldung.com — Introduction to Project Jigsaw

Alex Buckley — Modular Development with JDK 9

Евгений Козлов – Модули в Java