что такое модуль в java
Модульность
Создание модуля
До Java 9 было несколько уровней инкапсуляции функционала. Первый уровень представлял класс, в котором мы могли определить переменные и методы с различным уровнем доступа. Следующий уровень представлял пакет, который, в свою очередь, представлял коллекцию классов. Однако со временем этих уровней оказалось недостаточно. И модуль стал следующим уровнем инкапсуляции, который объединял несколько пакетов.
Модуль состоит из группы пакетов. Также модуль включает список все пакетов, которые входят в модуль, и список всех модулей, от которых зависит данный модуль. Дополнительно (но необязательно) он может включать файлы ресурсов или файлы нативных библиотек.
В качестве названия модуля может использоваться произвольный идентификатор из алфавитно-цифровых символов и знаков подчеркивания. Но рекомендуется, чтобы название модуля соответствовало названию, которого начинаются пакеты этого модуля.
В каталоге demo определим новый файл module-info.java со следующим кодом:
Этот файл представляет дескриптор модуля (module descriptor). Этот файл может содержать только определение модуля.
С помощью ключевого слова module определяется модуль, который называется demo, то есть так же, как и каталог, в котором данный файл расположен. После имени модуля с помощью фигурных скобок можно определить тело модуля, но в данном случае код модуля не содержит никаких инструкций.
В папке com/metanit/hello определим новый файл Hello.java :
В итоге у нас получится следующая стуктура проекта:
Теперь скомпилируем все это. Для этого вначале перейдем в командной строке/терминале к папке, в которой находится модуль demo.
Затем для компиляции модуля выполним следующую команду:
После компиляции модуля demo выполним программу с помощью следующей команды:
И на консоли отобразится сообщение «Hello Demo Module!»
Модули Java 9 и внедрение зависимостей: используем Guice
Вот и пятница, до выходных еще далеко, поэтому мы надеемся, что сравнительно сложный текст вас не очень смутит.
Складывается впечатление, что модульная организация Java 9 потребует от программиста недюжинной изобретательности, и один из перспективных вариантов адаптации к такому дивному новому миру — это внедрение зависимостей. Именно по этому поводу внятно и интересно высказался в блоге O’Reilly уважаемый Пол Бэккер (Paul Bakker), один из авторов книги «Java 9 Modularity»
Приятного чтения и не забудьте проголосовать пожалуйста!
В этой статье мы рассмотрим, как можно сочетать модульную систему Java 9, внедрение зависимостей и использовать сервисы, чтобы ослабить связь между модулями.
Практически невозможно представить такую базу кода на Java, где не было бы внедрения зависимостей. Поэтому, что неудивительно, внедрение зависимостей может серьезно пригодиться, чтобы ослабить связанность кода. Слабая связанность достигается путем сокрытия реализации. Ослабление связи – ключевой фактор для удобства поддержки и расширяемости кода. Фактически, в Java эта стратегия сводится к программированию на основе интерфейсов, а не конкретных типов.
В долгосрочной перспективе такое ослабление связи упрощает поддержку кода, поскольку ясно, что делает каждая часть кода, и каждый модуль можно менять, не затрагивая всю остальную систему и даже не вполне понимая, как именно она устроена. Это одна из ключевых концепций модульности. Если код организован в виде модулей, то становится проще модифицировать отдельные части системы, что очень кстати как для поддержки, так и для расширяемости. Обратите внимание: при проектировании в расчете на модульность готовая модульная система не требуется, но, если она будет, это сильно все упростит.
Всякий раз, разбивая подобную систему на модули, мы сталкиваемся с проблемой практического характера. Как добиться слабой связанности между CLI/GUI и анализаторами? Ведь в какой-то момент нам непременно потребуется создать экземпляр класса реализации. Классическое решение такой проблемы – внедрение зависимости, либо, иными словами, инверсия управления. Если мы воспользуемся внедрением зависимости, то наш код CLI/GUI просто объявит, что ему нужны экземпляры интерфейса Analyzer ; обычно это делается при помощи аннотаций. Фактическое инстанцирование классов реализации и привязка их к коду CLI/GUI выполняется при помощи фреймворка для внедрения зависимостей – популярными примерами таких фреймворков являются Spring и Guice. В этой статье используется Guice, но в книге Java 9 Modularity также есть подробный пример на основе Spring.
Что лучше при работе с модулями: внедрение зависимостей или инкапсуляция?
Java 9 и модульная система этой версии языка позволяет вывести «развязывание» кода на новый уровень. Раньше можно было программировать в расчете на интерфейсы, но по-настоящему скрыть классы реализации не удавалось. До версии Java 9 в Java, в сущности, было невозможно инкапсулировать классы в модуле (и даже объявить модуль, если на то пошло). Ситуация меняется с появлением модульной системы Java 9, однако, здесь же возникает ряд новых проблем при работе с фреймворками для внедрения зависимостей.
Если изучить внутреннее устройство фреймворков для внедрения зависимостей, то выясняется, что фреймворк требует доступа либо для глубокой рефлексии, либо для чтения к обоим классам реализации, которые нужно внедрить, а также доступа для глубокой рефлексии к тем классам, в которые предполагается внедрить этот экземпляр. При модульной организации систем такой подход работает плохо. Классы реализации должны внедряться в свой модуль, а это означает, что код, расположенный вне модуля, будет лишен доступа к этим классам (даже при применении рефлексии). Фреймворк для внедрения зависимостей – всего лишь еще один модуль, подчиняющийся все тем же правилам модульной системы, а это означает, что у фреймворка не будет доступа к этим классам. Таким образом, нам придется ослабить инкапсуляцию, а это нехорошо.
Рассмотрим типичную настройку Guice.
В этом главном методе осуществляется начальная загрузка фреймворка Guice с несколькими модулями Guice (не путайте их с модулями Java 9!). В каждом модуле есть одна или более реализаций для интерфейсов, которые мы собираемся внедрять. Например, модуль ColemanModule мог бы выглядеть так.
Рис. 1. Зависимости между модулями: налицо сильное связывание
Свидетельствуют ли эти новые проблемы, что с модулями сложно работать? Отнюдь! Модули наконец-то позволяют нам инкапсулировать код, и это серьезный шаг к такому проектированию «со слабым связыванием», к которому мы стремимся. Существующие фреймворки не рассчитаны на эти новые возможности, поэтому, возможно, нам потребуется немного пересмотреть работу с ними. Однако, сейчас я покажу, какой великолепный обходной маневр на данный случай предлагает Guice.
Прежде чем решить эту задачу, давайте рассмотрим, что предлагает сама модульная система, чтобы обеспечить работу с инкапсулированными типами реализациями между разными модулями.
Использование сервисов в качестве альтернативы внедрению зависимостей
В модульной системе встроена специальная возможность для ослабления связи между модулями. При помощи сервисов модуль может объявить, что в нем предоставляется реализация интерфейса. Другие модули могут объявлять, что используют этот интерфейс. Система модулей передает реализации тому модулю, который использует сервис, причем, модулю не требуется читать тип реализации, даже нет необходимости предоставлять зависимость от предоставляющего модуля.
Такой новый дизайн на основе сервисов представлен на следующем рисунке. Как видите, сервисы отлично подходят для ослабления связи между модулями, и, поскольку этот подход разработан специально для системы модулей, он не требует так же жертвовать инкапсуляцией, как пришлось бы при применении фреймворка для внедрения зависимостей вроде Guice. Сервисы не идентичны внедрению зависимостей, поскольку API ServiceLoader ищет реализации, а не внедряемые зависимости, но подход с сервисами решает ту же проблему. Во многих практических ситуациях разумнее воспользоваться сервисами, а не полагаться на внешние фреймворки.
Рис. 2. Ослабление связывания при помощи сервисов
Что, если мы все равно хотим использовать Guice, поскольку нам придется работать с уже имеющейся базой кода, основанной на Guice – либо если нам попросту нравится декларативная природа внедрения зависимостей? Можно ли лучше совместить этот фреймворк с модульной системой? Оказывается, комбинация с Guice – очень красивое решение!
Сочетание внедрения зависимостей с сервисами
Как мы уже убедились, основная проблема при работе с Guice – возникновение непосредственной связи между модулем CLI/GUI и модулями анализаторов. Все дело в том, что нам требуются классы AbstractModule для начальной загрузки Guice. Что, если бы удалось вообще исключить этот шаг и предоставлять классы AbstractModule в виде сервисов?
Пакет реализации все равно должен оставаться открыт для глубокой рефлексии, так как Guice необходима возможность инстанцировать классы. Невелика проблема, поскольку здесь мы не привносим в код никаких лишних связей, от которых стоило бы избавиться.
Итак, мы вкратце обсудили, как внедрение зависимостей помогает бороться со связанностью кода, и какие осложнения возможны при работе с имеющимися фреймворками внедрения зависимостей, например, с Guice, когда мы инкапсулируем наш код в модулях. Сервисы могут послужить встроенной альтернативой зависимостям, а комбинация сервисов и Guice удобна при внедрении зависимостей, и отказываться от инкапсуляции в таком случае также не приходится.
Весь исходный код для этой статьи выложен на GitHub. Там две ветки: одна с использованием сервисов, как показано в последнем примере, а другая – без них.
Модульность в Java 9
Основным нововведением Java 9 было именно введение модульности. Про эту фичу было много разговоров, дата релиза несколько раз переносилась, чтобы допилить все должным образом. В этом посте речь пойдет о том, что дает механизм модулей, и чего полезного Java 9 принесла в целом. Основой для поста послужил доклад моего коллеги — Сергея Малькевича.
Для реализации модулей в этой версии Java был выделен целый проект — Project Jigsaw — который включает в себя несколько JEP и JSR.
Для любителей официальной документации, ознакомиться более подробно с каждым JEP можно здесь.
Подробнее о Project Jigsaw
Project Jigsaw, который реализует модульность, начал разрабатываться в далеком 2005: сначала вышел JSR 277, а уже в 2008 началась непосредственная работа над проектом. Релиз состоялся только в 2017 году. То есть, для того, чтобы докрутить модули в Java, понадобилось почти 10 лет. Что, собственно, подчеркивает весь масштаб работы и изменений, которые были внесены в ходе реализации модульности.
Какие цели ставили перед собой разработчики:
Чего полезного принесла 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 у нас идет имя пакета jdk.javadoc, который зависит от другого пакета java.xml и транзитивно зависит от других пакетов.
Давайте подробнее пройдемся по каждому из ключевых слов:
Сначала указываем интерфейс — javax.tools.Tool, после with — реализацию.
Немного о сервисах
Допустим, у нас подключено несколько модулей, которые реализуют абстрактный сервис — MyService. При сборке приложения у нас есть возможность решить, какую реализацию сервиса использовать, “перетащив” нужные нам модули реализации сервиса на —module-path:
Таким образом, возвращенный Iterator содержит список реализаций интерфейса MyService. Фактически, он будет содержать все реализации, найденные в модулях, найденных на —module-path.
Зачем в принципе были введены сервисы? Они нужны для того, чтобы показать, как наш код будет использован. То есть, здесь заключена семантическая роль. Также, модульность — это про инкапсуляцию и безопасность, так как мы можем сделать реализацию private и исключить возможность несанкционированного доступа через reflection.
Также, один из вариантов использования сервисов — это достаточно простая реализация плагинов. Мы можем реализовать интерфейс плагина для нашего приложения и подключать модули для работы с ними.
Вернемся к синтаксису описания модулей:
До 9ки через reflection мы имели доступ практически ко всему и могли делать все, что хотим и с чем хотим. А 9-ая версия, как уже упоминалось, позволяет обезопасить себя от “нелегального” reflection доступа.
Мы можем полностью открыть модуль для reflection доступа, объявив open:
Либо, мы можем указать какие либо пакеты для reflection доступа, объявив opens:
Здесь же есть возможность использовать opens com.my.coolpackage to…, таким образом предоставляя reflection доступ пакету com.my.coolpackage из пакета, который укажем после to.
Типы модулей
Project Jigsaw классифицирует модули следующим образом:
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 и так далее. Но большинство библиотек так и не перешло на полное использование модулей. Видимо поэтому, все эти изменения были восприняты довольно скептически техничесим сообществом. Модули предоставляют нам новые возможности, но вопрос востребованности остаётся открытым.
Ну и напоследок, предлагаю подборку материалов на эту тему:
Руководство по модульности Java 9
1. Обзор
Java 9 представляет новый уровень абстракции над пакетами, формально известный как система модулей платформы Java (JPMS), или для краткости «модули».
В этом руководстве мы рассмотрим новую систему и обсудим ее различные аспекты.
Мы также создадим простой проект, чтобы продемонстрировать все концепции, которые мы изучим в этом руководстве.
2. Что такое модуль?
Прежде всего, нам нужно понять, что такое модуль, прежде чем мы сможем понять, как их использовать.
Другими словами, это абстракция «пакета Java-пакетов», которая позволяет нам сделать наш код еще более пригодным для повторного использования.
2.1. Пакеты
Пакеты внутри модуля идентичны пакетам Java, которые мы использовали с момента создания Java.
Когда мы создаем модуль, мы организуем код внутри пакетов, как мы делали это раньше с любым другим проектом.
Помимо организации нашего кода, пакеты используются для определения того, какой код является общедоступным вне модуля. Мы поговорим об этом позже в статье.
2.2. Ресурсы
Каждый модуль отвечает за свои ресурсы, такие как медиа или файлы конфигурации.
Раньше мы помещали все ресурсы на корневой уровень нашего проекта и вручную управляли тем, какие ресурсы принадлежали разным частям приложения.
С помощью модулей мы можем отправлять необходимые изображения и XML-файлы с модулем, который в них нуждается, что значительно упрощает управление нашими проектами.
2.3. Дескриптор модуля
Когда мы создаем модуль, мы включаем файл дескриптора, который определяет несколько аспектов нашего нового модуля:
Нам нужно перечислить все пакеты, которые мы хотим сделать общедоступными, потому что по умолчанию все пакеты являются частными модулями.
То же верно и для размышлений. По умолчанию мы не можем использовать отражение для классов, которые мы импортируем из другого модуля.
Позже в статье мы рассмотрим примеры использования файла дескриптора модуля.
2.4. Типы модулей
В новой модульной системе есть четыре типа модулей:
2.5. Распределение
Модули можно распространять одним из двух способов: как файл JAR или как «разобранный» скомпилированный проект. Это, конечно, то же самое, что и любой другой проект Java, поэтому неудивительно.
Мы можем создавать многомодульные проекты, состоящие из «основного приложения» и нескольких библиотечных модулей.
Мы должны быть осторожны, потому что у нас может быть только один модуль на файл JAR.
Когда мы настраиваем наш файл сборки, нам нужно убедиться, что каждый модуль в нашем проекте объединен как отдельный jar.
3. Модули по умолчанию
Когда мы устанавливаем Java 9, мы видим, что JDK теперь имеет новую структуру.
Они взяли все исходные пакеты и переместили их в новую модульную систему.
Мы можем увидеть, что это за модули, набрав в командной строке:
java modules are the implementation classes for the core SE Language Specification.
javafx modules are the FX UI libraries.
Anything needed by the JDK itself is kept in the jdk modules.
And finally, anything that is Oracle-specific is in the oracle modules.
4. Module Declarations
To set up a module, we need to put a special file at the root of our packages named module-info.java.
This file is known as the module descriptor and contains all of the data needed to build and use our new module.
We construct the module with a declaration whose body is either empty or made up of module directives:
We start the module declaration with the module keyword, and we follow that with the name of the module.
The module will work with this declaration, but we’ll commonly need more information.
That is where the module directives come in.
4.1. Requires
Our first directive is requires. This module directive allows us to declare module dependencies:
Now, my.module has both a runtime and a compile-time dependency on module.name.
And all public types exported from a dependency are accessible by our module when we use this directive.
4.2. Requires Static
Sometimes we write code that references another module, but that users of our library will never want to use.
For instance, we might write a utility function that pretty-prints our internal state when another logging module is present. But, not every consumer of our library will want this functionality, and they don’t want to include an extra logging library.
In these cases, we want to use an optional dependency. By using the requires static directive, we create a compile-time-only dependency:
4.3. Requires Transitive
We commonly work with libraries to make our lives easier.
But, we need to make sure that any module that brings in our code will also bring in these extra ‘transitive’ dependencies or they won’t work.
Luckily, we can use the requires transitive directive to force any downstream consumers also to read our required dependencies:
Now, when a developer requires my.module, they won’t also have also to say requires module.name for our module to still work.
4.4. Exports
By default, a module doesn’t expose any of its API to other modules. This strong encapsulation was one of the key motivators for creating the module system in the first place.
Our code is significantly more secure, but now we need to explicitly open our API up to the world if we want it to be usable.
We use the exports directive to expose all public members of the named package:
Now, when someone does requires my.module, they will have access to the public types in our com.my.package.name package, but not any other package.
4.5. Exports … To
We can use exports…to to open up our public classes to the world.
But, what if we don’t want the entire world to access our API?
We can restrict which modules have access to our APIs using the exports…to directive.
Similar to the exports directive, we declare a package as exported. But, we also list which modules we are allowing to import this package as a requires. Let’s see what this looks like:
4.6. Uses
A service is an implementation of a specific interface or abstract class that can be consumed by other classes.
We designate the services our module consumes with the uses directive.
Note that the class name we use is either the interface or abstract class of the service, not the implementation class:
We should note here that there’s a difference between a requires directive and the uses directive.
We might require a module that provides a service we want to consume, but that service implements an interface from one of its transitive dependencies.
Instead of forcing our module to require all transitive dependencies just in case, we use the uses directive to add the required interface to the module path.
4.7. Provides … With
A module can also be a service provider that other modules can consume.
The first part of the directive is the provides keyword. Here is where we put the interface or abstract class name.
Next, we have the with directive where we provide the implementation class name that either implements the interface or extends the abstract class.
Here’s what it looks like put together:
4.8. Open
We mentioned earlier that encapsulation was a driving motivator for the design of this module system.
Before Java 9, it was possible to use reflection to examine every type and member in a package, even the private ones. Nothing was truly encapsulated, which can open up all kinds of problems for developers of the libraries.
Because Java 9 enforces strong encapsulation, we now have to explicitly grant permission for other modules to reflect on our classes.
If we want to continue to allow full reflection as older versions of Java did, we can simply open the entire module up:
4.9. Opens
If we need to allow reflection of private types, but we don’t want all of our code exposed, we can use the opens directive to expose specific packages.
But remember, this will open the package up to the entire world, so make sure that is what you want:
4.10. Opens … To
Okay, so reflection is great sometimes, but we still want as much security as we can get from encapsulation. We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive:
5. Command Line Options
By now, support for Java 9 modules has been added to Maven and Gradle, so you won’t need to do a lot of manual building of your projects. However, it’s still valuable to know how to use the module system from the command line.
We’ll be using the command line for our full example down below to help solidify how the entire system works in our minds.
6. Visibility
We should spend a little time talking about the visibility of our code.
A lot of libraries depend on reflection to work their magic (JUnit and Spring come to mind).
By default in Java 9, we will only have access to public classes, methods, and fields in our exported packages. Even if we use reflection to get access to non-public members and call setAccessible(true), we won’t be able to access these members.
We can use the open, opens, and opens…to options to grant runtime-only access for reflection. Note, this is runtime-only!
We won’t be able to compile against private types, and we should never need to anyway.
If we must have access to a module for reflection, and we’re not the owner of that module (i.e., we can’t use the opens…to directive), then it’s possible to use the command line –add-opens option to allow own modules reflection access to the locked down module at runtime.
The only caveat here’s that you need to have access to the command line arguments that are used to run a module for this to work.
7. Putting It All Together
Now that we know what a module is and how to use them let’s go ahead and build a simple project to demonstrate all the concepts we just learned.
To keep things simple, we won’t be using Maven or Gradle. Instead, we’ll rely on the command line tools to build our modules.
7.1. Setting Up Our Project
First, we need to set up our project structure. We’ll create several directories to organize our files.
Start by creating the project folder:
This is the base of our whole project, so add files in here such as Maven or Gradle build files, other source directories, and resources.
We also put a directory to hold all our project specific modules.
Next, we create a module directory:
Here’s what our project structure will look like:
7.2. Our First Module
Now that we have the basic structure in place, let’s add our first module.
Under the simple-modules directory, create a new directory called hello.modules.
We can name this anything we want but follow package naming rules (i.e., periods to separate words, etc.). We can even use the name of our main package as the module name if we want, but usually, we want to stick to the same name we would use to create a JAR of this module.
Under our new module, we can create the packages we want. In our case, we are going to create one package structure:
Next, create a new class called HelloModules.java in this package. We will keep the code simple:
And finally, in the hello.modules root directory, add in our module descriptor; module-info.java:
To keep this example simple, all we are doing is exporting all public members of the com.baeldung.modules.hello package.
7.3. Our Second Module
Our first module is great, but it doesn’t do anything.
We can create a second module that uses it now.
Under our simple-modules directory, create another module directory called main.app. We are going to start with the module descriptor this time:
We don’t need to expose anything to the outside world. Instead, all we need to do is depend on our first module, so we have access to the public classes it exports.
Now we can create an application that uses it.
Create a new package structure: com.baeldung.modules.main.
Now, create a new class file called MainApp.java.
And that is all the code we need to demonstrate modules. Our next step is to build and run this code from the command line.
7.4. Building Our Modules
To build our project, we can create a simple bash script and place it at the root of our project.
Create a file called compile-simple-modules.sh:
There are two parts to this command, the javac and find commands.
The only thing we have to do differently than the older versions of Java is to provide a module-source-path parameter to inform the compiler that it’s building modules.
Once we run this command, we will have an outDir folder with two compiled modules inside.
7.5. Running Our Code
And now we can finally run our code to verify modules are working correctly.
Create another file in the root of the project: run-simple-module-app.sh.
To run a module, we must provide at least the module-path and the main class. If all works, you should see:
7.6. Adding a Service
Now that we have a basic understanding of how to build a module, let’s make it a little more complicated.
We’re going to see how to use the provides…with and uses directives.
Start by defining a new file in the hello.modules module named HelloInterface.java:
To make things easy, we’re going to implement this interface with our existing HelloModules.java class:
That is all we need to do to create a service.
Now, we need to tell the world that our module provides this service.
Add the following to our module-info.java:
As we can see, we declare the interface and which class implements it.
Next, we need to consume this service. In our main.app module, let’s add the following to our module-info.java:
Finally, in our main method we can use this service via a ServiceLoader:
We use these directives to be much more explicit about how our code is to be used.
We could put the implementation into a private package while exposing the interface in a public package.
This makes our code much more secure with very little extra overhead.
Go ahead and try out some of the other directives to learn more about modules and how they work.
8. Adding Modules to the Unnamed Module
The unnamed module concept is similar to the default package. Therefore, it’s not considered a real module, but can be viewed as the default module.
If a class is not a member of a named module, then it will be automatically considered as part of this unnamed module.
Sometimes, to ensure specific platform, library, or service-provider modules in the module graph, we need to add modules to the default root set. For example, when we try to run Java 8 programs as-is with Java 9 compiler we may need to add modules.
In general, the option to add the named modules to the default set of root modules is –add-modules (,)* where is a module name.
For example, to provide access to all java.xml.bind modules the syntax would be:
To use this in Maven, we can embed the same to the maven-compiler-plugin:
9. Conclusion
В этом обширном руководстве мы сосредоточились и рассмотрели основы новой системы модулей Java 9.
Мы начали с разговора о том, что такое модуль.
Далее мы поговорили о том, как узнать, какие модули включены в JDK.
Мы также подробно рассмотрели файл объявления модуля.
Мы завершили теорию, поговорив о различных аргументах командной строки, которые нам понадобятся для создания наших модулей.
Наконец, мы применили все наши предыдущие знания на практике и создали простое приложение, построенное на основе модульной системы.
Чтобы увидеть этот и другие коды, обязательно ознакомьтесь с ним на Github.