Зачем Java стал модульным?

Программисту на заметку
26301
0
Сергей Хоменко, ведущий программист департамента аналитических систем, R-Style Softlab
Программисты на Java хорошо знают одну его особенность: для использования кода, написанного на этом языке, всегда требуется наличие платформы (так называемой среды исполнения Java Runtime Environment). Из-за монолитности платформы до Java 9 любая программа содержала в себе большое количество неиспользуемых классов самой платформы. Теперь же ситуация изменилась — в Java «из коробки» поддерживается модульная архитектура, и первое место, где она используется, — пакеты Java Development Kit (JDK).


На протяжении уже многих лет Java остается самым популярным языком программирования в мире, о чем, в частности, свидетельствуют данные рейтинга TIOBE (рис. 1).

рис1.jpg

Рис. 1. Топ-10 языков программирования в рейтинге TIOBE, опубликованном в апреле 2018 г.

И неудивительно, ведь он обеспечивает обратную совместимость почти на 100% (что очень важно для корпоративных решений), предоставляет программистам широкие возможности, независим от платформы (написанные на нем программы могут работать в любой среде), обладает огромным сообществом пользователей. Одним из самых ожидаемых нововведений в Java 9 стала поддержка проекта Jigsaw (рис. 2).

рис2.png

Рис. 2. Самые ожидаемые новые возможности в Java 9 (схема позаимствована из статьи Alex Zhitnitsky «What Java 9 features are developers yearning for most?», опубликованной на портале JAXenter)

Сообщество, в свою очередь, прилагало усилия для внедрения модульности в программы, написанные на Java, — например, в OSGi или JBoss Modules. Однако официальных инструментов от Sun/Oracle не выпускалось никогда, а попыток разбить на модули саму JDK не было вовсе. До недавнего времени...

Судьбоносной датой можно считать 21 сентября 2017 года — в этот день Oracle выпустила первый стабильный релиз Java 9, в котором была реализована поддержка модульности. Это сделало платформу более гибкой, производительной и защищенной за счет разбиения JDK на модули и внедрения возможности использования модульной архитектуры в создаваемых решениях.


Преимущества модульности в Java


Кроме прочих нововведений, таких как исправление ошибок, изменение/улучшение API, изменение в GC, поддержка HTTP/2 клиента и, конечно же, обновленная консоль, ключевой особенностью нового релиза стало развитие проекта Jigsaw в Java Platform Module System (JPMS).

Перечислю основные преимущества, которых удалось добиться внедрением модульности:

  • Надежная (однозначная) конфигурация приложения. Механизм «модульности» предоставляет способ однозначно определить зависимости между модулями. Такие зависимости могут быть распознаны и во время компиляции кода, и в процессе его исполнения. Таким образом, система может в любой момент времени определить, какие наборы модулей необходимы для работы приложения в целом.
  • Усиленная инкапсуляция. Пакеты в модулях доступны извне только в том случае, если модуль явно их экспортировал. Но даже тогда другой модуль не сможет использовать такие пакеты, поскольку для этого нужно явно выразить это намерение.
  • Масштабируемая Java-платформа. Ранее, чтобы запустить Java-приложение, нужна была среда Java Runtime Environment (JRE) целиком, даже если это приложение все лишь выводило на экран сакраментальную фразу «Hello World». Теперь платформа состоит из набора модулей (я насчитал 95 штук), и разработчик может решить сам, какие из них понадобятся его приложению.
  • Повышенная целостность платформы. До выхода Java 9 была возможность использовать любые классы JDK. Теперь же, учитывая более сильную инкапсуляцию при реализации модулей, из которых состоит и JDK, использование классов, описывающих детали реализации за пределами модуля, становится фактически невозможным.
  • Рост производительности. Как известно, виртуальная машина Java (JVM) использует различные механизмы оптимизации. Производители утверждают, что эти механизмы работают лучше, если система заранее знает все требуемые/используемые зависимости приложения.


Использование модулей

 
Интересно узнать, из каких модулей состоит установленная у вас версия JDK? Используйте ключ list-modules. Именно так мне открылось знание о наличии в моей JDK 95 модулей.

Чтобы система могла идентифицировать пакет как модуль, он должен содержать module declaration — файл module-info.java. В нем может находиться следующая информация: имя модуля; сведения о зависимостях; о пакетах, которые экспортирует модуль; список предоставляемых/потребляемых этим модулем сервисов; степень доступности через рефлексию. Располагаться файл module-info.java должен в корневом каталоге пакета, а его минимальное содержание может быть таким:

module BARModule {
}


Для файла описания модуля в язык введены новые специальные ключевые слова: exports, module, open, opens, provides, requires, uses, with, to, transitive. С целью поддержки обратной совместимости «ключевыми» они являются только в module declaration, и это замечательно, так как их использование в вашем коде не повлияет на успешность компиляции.

requires – указывает на зависимость от другого модуля (это то, что подразумевается под усиленной инкапсуляцией):

module FOOModule {
requires BARModule;
}


В англоязычных источниках глядя на эти строки сказали бы, что модуль FOOModule read BARModule, мне же привычнее говорить, что модуль FOOModule зависит от модуля BARModule.

Не стоит забывать, что модуль BARModule должен быть добавлен в зависимости.

Кроме того, описывая зависимости, можно указать еще одно ключевое слово — transitive, которое позволит модулям, использующим FOOModule, использовать и BARModule без необходимости явно объявлять эту зависимость в module declaration. В противном случае такой код просто не скомпилируется:

module FOOModule {
    requires transitive BARModule;
}


exports – определяет пакет модуля, в котором классы с модификатором public будут доступны вне этого модуля (кроме самих классов будут также доступны их внутренние public и protected):

module BARModule {
    exports BARModule.ModuleExport;
}


Кроме того доступно расширение синтаксиса:

module BARModule {
    exports BARModule.ModuleExport to FOOModule, BAZModule;
}


Указав через запятую после слова to перечень модулей, вы, таким образом, ограничите модули, которые могут использовать экспортируемый модуль.

provides…with – определяет модуль как реализацию некоего сервиса. То есть модуль становится service provider. Часть provides определяет интерфейс или абстрактный класс, а with определяет класс, реализующий обещанную функциональность:

module BARModule {
    exports BARModule.ModuleExport to FOOModule, BAZModule;
    provides BARModule.ModuleExport.BARServiceInterface with
BARModule.ModuleExport.PublicBARServiceImplementation;
}


Директива uses определяет сервис, используемый модулем. Модуль в таком случае становится service consumer:

module FOOModule {
    requires transitive BARModule;
    uses BARModule.ModuleExport.BARServiceInterface;
}


До Java 9, используя рефлексию, можно было получить информацию обо всех компонентах пакета/класса, в том числе приватных, а также изменять модификаторы и пр. Теперь же, когда сильная инкапсуляция стала одной из ключевых особенностей модулей, решить проблему её нарушения призваны ключевые слова open, opens и расширение to:

module BARModule {
    exports BARModule.ModuleExport to FOOModule, BAZModule;
    opens BARModule.ModuleExport;
    provides BARModule.ModuleExport.BARServiceInterface with BARModule.ModuleExport.PublicBARServiceImplementation;
}


Данный код сделает пакет и public классы в нем (и по аналогии exports его public и protected внутренние классы) доступными во время выполнения. Кроме того, все содержимое пакета будет доступным через рефлексию.

module BARModule {
    exports BARModule.ModuleExport to FOOModule, BAZModule;
    opens BARModule.ModuleOpens to BAZModule;
    provides BARModule.ModuleExport.BARServiceInterface with BARModule.ModuleExport.PublicBARServiceImplementation;
}


Расширением to задаются модули (список через запятую), для которых opens будет работать.

Последний, самый радикальный, способ открыть рефлексию в модуле – объявить весь модуль open. Тогда все пакеты и классы этого модуля будут доступны для использования во время выполнения программы и для рефлексии:

open module FOOModule {
     requires transitive BARModule;
     uses BARModule.ModuleExport.BARServiceInterface;
}

Как разработчик корпоративных продуктов должен отметить, что модульная архитектура особенно полезна в применении к большим проектам, реализующим огромное количество различных взаимодействий с пользователем.
Она позволяет делать разрабатываемую систему более управляемой, расширяемой, поддерживаемой. Кроме того, благодаря усиленной инкапсуляции, достигаемой при использовании системы модулей, можно оградить код от «неправильного использования», выставив наружу только API. Чётко определив зависимости еще на этапе написания кода, вы сможете быть уверенными в том, что в процессе эксплуатации система никогда внезапно не упадёт по причине отсутствия нужного класса в classpath. Я уж не говорю о том, как полезно это знание при компиляции.

Все статьи

Комментарии



Подписка на рассылку
Сортировать
Теги:
Все теги
Выберите интересующий Вас продукт компании
Любой продукт
Сортировать по году:
2018