Модульность
С выходом JDK 9 в языке Java появилась новая возможность — модульность. Модульность позволяет разбить код на отдельные структурные единицы — модули. Фактически модуль представляет группу пакетов или ресурсов, объединенных в одно целое и к которым можно обращаться по имени модуля.
До Java 9 было несколько уровней инкапсуляции функционала. Первый уровень представлял класс, в котором мы могли определить переменные и методы с различным уровнем доступа. Следующий уровень представлял пакет, который, в свою очередь, представлял коллекцию классов. Однако со временем этих уровней оказалось недостаточно. И модуль стал следующим уровнем инкапсуляции, который объединял несколько пакетов.
Модуль состоит из группы пакетов. Также модуль включает список все пакетов, которые входят в модуль, и список всех модулей, от которых зависит данный модуль. Дополнительно (но необязательно) он может включать файлы ресурсов или файлы нативных библиотек.
В качестве названия модуля может использоваться произвольный идентификатор из алфавитно-цифровых символов и знаков подчеркивания. Но рекомендуется, чтобы название модуля соответствовало названию, которого начинаются пакеты этого модуля.
Определим и используем простейший модуль. Допустим, файлы с исходными кодами помещаются в папку C:\java (либо какую-нибудь другую папку на жестком диске). Создадим в этой папке каталог, который назовем demo . Этот каталог будет представлять модуль.
В каталоге demo определим новый файл module-info.java со следующим кодом:
module demo
Этот файл представляет дескриптор модуля (module descriptor). Этот файл может содержать только определение модуля.
С помощью ключевого слова module определяется модуль, который называется demo, то есть так же, как и каталог, в котором данный файл расположен. После имени модуля с помощью фигурных скобок можно определить тело модуля, но в данном случае код модуля не содержит никаких инструкций.
Далее в каталоге demo создадим папку com . В папке com создадим папку metanit , а в папке com/metanit — папку hello .
В папке com/metanit/hello определим новый файл Hello.java :
package com.metanit.hello; public class Hello < public static void main(String[] args)< System.out.println("Hello Demo Module!"); >>
Название пакета файла — com.metanit.hello отражает структуру папок, в которых расположен файл. Сам файл определяет класс Hello, который в методе main выводит на консоль строку.
В итоге у нас получится следующая стуктура проекта:

Теперь скомпилируем все это. Для этого вначале перейдем в командной строке/терминале к папке, в которой находится модуль demo.
Затем для компиляции модуля выполним следующую команду:
javac demo/module-info.java demo/com/metanit/hello/Hello.java
После компиляции модуля demo выполним программу с помощью следующей команды:
java --module-path demo --module demo/com.metanit.hello.Hello
Параметр —module-path указывает на путь к модулю, а —module — на главный класс модуля.
При наборе команды вместо параметра —module-path можно указать его сокращение -p , а вместо параметра —module — сокращение -m .
И на консоли отобразится сообщение «Hello Demo Module!»
Общие сведения о модулях Java | Java для начинающих
Главный инженер программного обеспечения Майкрософт для Java и чемпион Java Кирк Пеппедин представляет нам модули Java, функцию в Java 9 и более познакомит с ней. Модуль Java — это набор пакетов и определенных типов Java, включая классы, интерфейсы и многое другое, упакованные с файлами данных и статическими ресурсами.
Рекомендуемые ресурсы
- Ознакомьтесь с остальными выпусками серии «Java для начинающих»
- Примеры и вспомогательные материалы
- Рекомендуемые модули Microsoft Learn
Главный инженер программного обеспечения Майкрософт для Java и чемпион Java Кирк Пеппедин представляет нам модули Java, функцию в Java 9 и более познакомит с ней. Модуль Java — это набор пакетов и определенных типов Java, включая классы, интерфейсы и многое другое, упакованные с файлами данных и статическими ресурсами.
Рекомендуемые ресурсы
- Ознакомьтесь с остальными выпусками серии «Java для начинающих»
- Примеры и вспомогательные материалы
- Рекомендуемые модули Microsoft Learn
Первые шаги с Java 9 и проект Jigsaw – часть первая
Еще со времен книги «Java. Новое поколение разработки» мы следим за развитием давно анонсированных новых возможностей этого языка, объединенных под общим названием «Project Jigsaw». Сегодня предлагаем перевод статьи от 24 ноября, вселяющей достаточную уверенность, что в версии Java 9 Jigsaw все-таки состоится.
Прошло восемь лет после зарождения проекта Jigsaw, задача которого заключается в модуляризации платформы Java и сводится к внедрению общей системы модулей. Предполагается, что Jigsaw впервые появится в версии Java 9. Этот релиз ранее планировался и к выходу Java 7, и к Java 8. Область применения Jigsaw также неоднократно менялась. Теперь есть все основания полагать, что Jigsaw практически готов, поскольку ему было уделено большое внимание в пленарном докладе Oracle на конференции JavaOne 2015, а также сразу несколько выступлений на эту тему. Что это означает для вас? Что такое проект Jigsaw и как с ним работать?
Это первая из двух публикаций, в которых я хочу сделать краткое введение в систему модулей и на многочисленных примерах кода продемонстрировать поведение Jigsaw. В первой части мы обсудим, что представляет собой система модулей, как был модуляризован JDK, а также рассмотрим поведение компилятора и исполняющей среды в определенных ситуациях.
Что такое модуль?
Описать модуль просто: это единица в составе программы, причем каждый модуль сразу содержит ответы на три вопроса. Эти ответы записаны в файле module-info.java
, который есть у каждого модуля.
- Как называется модуль?
- Что он экспортирует?
- Что для этого требуется?

Простой модуль
Ответ на первый вопрос несложен. (Почти) у каждого модуля есть имя. Оно должно соответствовать соглашениям об именовании пакетов, например, de.codecentric.mymodule
, во избежание конфликтов.
Для ответа на второй вопрос в модуле предоставляется список всех пакетов данного конкретного модуля, которые считаются публичными API и, следовательно, могут использоваться другими модулями. Если класс не является экспортированным пакетом, никто не может получить к нему доступ извне вашего модуля — даже если он публичный.
Ответ на третий вопрос — это список тех модулей, от которых зависит данный модуль. Все публичные типы, экспортируемые данными модулями, доступны зависимому модулю. Команда Jigsaw старается ввести в обиход термин «считывать» другой модуль.
Это серьезное изменение статус-кво. Вплоть до Java 8 включительно любой публичный тип в пути к вашим классам был доступен любому другому типу. С приходом Jigsaw система доступности типов Java изменяется с
- публичный для всех, кто читает этот модуль (exports)
- публичный для некоторых модулей, читающих этот (exports to, об этом пойдет речь во второй части)
- публичный для любого другого класса в рамках данного модуля
- private
- default
- protected
Модуляризованный JDK
Зависимости модулей должны образовывать ациклический граф, не допуская, таким образом, циклических зависимостей. Чтобы реализовать такой принцип, команде Jigsaw предстояло решить следующую большую задачу: разбить на модули исполняющую среду Java, которая, как сообщалось, полна циклических и нелогичных зависимостей. Получился такой граф:

В основании графа находится java.base. Это единственный модуль, у которого есть только входящие ребра. Каждый создаваемый вами модуль считывает java.base
независимо от того, объявляете вы это или нет – как и в случае подразумеваемого расширения java.lang.Object
. java.base
экспортирует такие пакеты как java.lang
, java.util
, java.math
и т.д.
Модуляризация JDK означает, что теперь вы можете указывать, какие модули исполняющей среды Java, которые хотите использовать. Так, ваше приложение не должно задействовать среду, поддерживающую Swing или Corba, если вы не читаете модулей java.desktop
или java.corba
. Создание такой урезанной среды будет описано во второй части.
Но довольно сухой теории…
Весь код, приведенный в статье, доступен здесь, включая сценарии оболочки для компиляции, упаковки и запуска примера.
Рассматриваемый здесь практический случай очень прост. У меня есть модуль de.codecentric.zipvalidator
, выполняющий определенную валидацию zip-кода. Этот модуль считывается модулем de.codecentric.addresschecker
(который мог бы проверять отнюдь не только zip-коды, но здесь мы этого не делаем, чтобы не усложнять).
Zip-валидатор описан в следующем файле module-info.java
:
module de.codecentric.zipvalidator exports de.codecentric.zipvalidator.api;
>
Итак, этот модуль экспортирует пакет de.codecentric.zipvalidator.api
и не читает никаких других модулей (кроме java.base
). Этот модуль считывается addresschecker’ом:
module de.codecentric.addresschecker
Общая структура файловой системы такова:
two-modules-ok/ ├── de.codecentric.addresschecker │ ├── de │ │ └── codecentric │ │ └── addresschecker │ │ ├── api │ │ │ ├── AddressChecker.java │ │ │ └── Run.java │ │ └── internal │ │ └── AddressCheckerImpl.java │ └── module-info.java ├── de.codecentric.zipvalidator │ ├── de │ │ └── codecentric │ │ └── zipvalidator │ │ ├── api │ │ │ ├── ZipCodeValidator.java │ │ │ └── ZipCodeValidatorFactory.java │ │ ├── internal │ │ │ └── ZipCodeValidatorImpl.java │ │ └── model │ └── module-info.java
Действует соглашение, согласно которому модуль помещается в каталоге, одноименном этому модулю.
В первом примере все выглядит отлично: мы работаем строго по правилам и в нашем классе AddressCheckerImpl
обращаемся только к ZipCodeValidator
и ZipCodeValidatorFactory
из экспортированного пакета:
public class AddressCheckerImpl implements AddressChecker < @Override public boolean checkZipCode(String zipCode) < return ZipCodeValidatorFactory.getInstance().zipCodeIsValid(zipCode); >>
Теперь запустим javac
и сгенерируем байт-код. Чтобы скомпилировать zipvalidator
(что нам, разумеется, нужно сделать в первую очередь, так как addresschecker считывает zipvalidator), мы делаем
javac -d de.codecentric.zipvalidator \ $(find de.codecentric.zipvalidator -name "*.java")
Выглядит знакомо – пока нет речи о модулях, поскольку zipvalidator не зависит ни от одного пользовательского модуля. Команда find
просто помогает нам составить список файлов .java
в указанном каталоге.
Но как мы сообщим javac
о структуре наших модулей, когда дойдем до компиляции? Для этого в Jigsaw вводится переключатель — modulepath
или -mp
.
Чтобы скомпилировать addresschecker, мы используем следующую команду:
javac -modulepath. -d de.codecentric.addresschecker \
$(find de.codecentric.addresschecker -name «*.java»)
При помощи modulepath мы сообщаем javac, где найти скомпилированные модули (в данном случае, это .), получается нечто похожее на переключатель пути к классам (classpath switch).
Однако компиляция множества модулей по отдельности кажется какой-то морокой – лучше воспользоваться другим переключателем -modulesourcepath, чтобы скомпилировать сразу несколько модулей:
javac -d . -modulesourcepath . $(find . -name "*.java")
Этот код ищет среди всех подкаталогов. каталоги модулей и компилирует все содержащиеся в них java-файлы.
Все скомпилировав, мы, естественно, хотим попробовать, что получилось:
java -mp . -m de.codecentric.addresschecker/de.codecentric.addresschecker.api.Run 76185
Опять же, мы указываем путь к модулям, сообщая JVM, где находятся скомпилированные модули. Также задаем основной класс (и параметр).
Ура, вот и вывод:
76185 is a valid zip code
Модульные Jar
Как известно, в мире Java мы привыкли получать и отправлять наш байт-код в jar-файлах. В Jigsaw вводится концепция модульного jar.Модульный jar очень похож на обычный, но в нем также содержится скомпилированный module-info.class.
При условии, что такие файлы скомпилированы для нужной целевой версии, эти архивы будут обратно совместимы. module-info.java
– не действительное имя типа, поэтому скомпилированный module-info.class
будет игнорироваться более старыми JVM.
Чтобы собрать jar для zipvalidator, пишем:
jar --create --file bin/zipvalidator.jar \ --module-version=1.0 -C de.codecentric.zipvalidator
Указываем файл вывода, версию (хотя отдельно не оговаривается использование нескольких версий модуля в Jigsaw во время исполнения) и модуль, который следует упаковать.
Поскольку у addresschecker также есть основной класс, мы можем указать и его:
jar --create --file=bin/addresschecker.jar --module-version=1.0 \ --main-class=de.codecentric.addresschecker.api.Run \ -C de.codecentric.addresschecker .
Основной класс не указывается в module-info.java
, как можно было бы ожидать (изначально команда Jigsaw так и планировала поступить), а обычно записывается в манифесте.
Если запустить этот пример с
java -mp bin -m de.codecentric.addresschecker 76185
получим такой же ответ, как и в предыдущем случае. Мы вновь указываем путь к модулям, который в данном случае ведет к каталогу bin, куда мы записали наши jars. Нам не приходится указывать основной класс, так как в манифесте addresschecker.jar уже есть эта информация. Достаточно сообщить имя модуля переключателю -m
.
До сих пор все было легко и приятно. Далее давайте немного повозимся с модулями и посмотрим, как Jigsaw ведет себя во время компиляции и исполнения, если вы начинаете хулиганить.
Использование неэкспортированных типов
В этом примере посмотрим, что происходит, если мы обращаемся к такому типу из другого модуля, который не должны использовать.
Поскольку мы устали от этой фабричной штуки в AddressCheckerImpl
, меняем реализацию на
return new ZipCodeValidatorImpl().zipCodeIsValid(zipCode);
При попытке скомпилировать получаем ожидаемое
error: ZipCodeValidatorImpl is not visible because package de.codecentric.zipvalidator.internal is not visible
Итак, именно использование неэкспортированных типов во время компиляции не срабатывает.
Но мы-то умные ребята, поэтому немного схитрим и используем рефлексию.
ClassLoader classLoader = AddressCheckerImpl.class.getClassLoader(); try < Class aClass = classLoader.loadClass("de.[..].internal.ZipCodeValidatorImpl"); return ((ZipCodeValidator)aClass.newInstance()).zipCodeIsValid(zipCode); >catch (Exception e)
Скомпилировалось отлично, давайте запускать. Ан нет, не так-то просто одурачить Jigsaw:
java.lang.IllegalAccessException: class de.codecentric.addresschecker.internal.AddressCheckerImpl (in module de.codecentric.addresschecker) cannot access class [..].internal.ZipCodeValidatorImpl (in module de.codecentric.zipvalidator) because module de.codecentric.zipvalidator does not export package de.codecentric.zipvalidator.internal to module de.codecentric.addresschecker
Итак, Jigsaw включает проверку не только во время компиляции, но и во время выполнения! Причем предельно четко сообщает нам, что мы сделали неправильно.
Циклические зависимости
В следующем случае мы вдруг осознали, что в API модуля addresschecker содержится класс, которым вполне мог бы воспользоваться zipvalidator. Поскольку мы ленивы, вместо рефакторинга класса в другой модуль мы объявляем зависимость для addresschecker:
module de.codecentric.zipvalidator
Поскольку циклические зависимости запрещены по определению, на нашем пути (ради общего блага) встает компилятор:
./de.codecentric.zipvalidator/module-info.java:2: error: cyclic dependence involving de.codecentric.addresschecker
Так делать нельзя, и нас заранее об этом предупреждают, еще во время компиляции.
Подразумеваемая считываемость
Чтобы расширить функционал, мы решаем унаследовать zipvalidator, введя новый модуль de.codecentric.zipvalidator.model
, содержащий определенную модель результата валидации, а не просто банальный булеан. Новая структура файла показана здесь:
three-modules-ok/ ├── de.codecentric.addresschecker │ ├── de │ │ └── codecentric │ │ └── addresschecker │ │ ├── api │ │ │ ├── AddressChecker.java │ │ │ └── Run.java │ │ └── internal │ │ └── AddressCheckerImpl.java │ └── module-info.java ├── de.codecentric.zipvalidator │ ├── de │ │ └── codecentric │ │ └── zipvalidator │ │ ├── api │ │ │ ├── ZipCodeValidator.java │ │ │ └── ZipCodeValidatorFactory.java │ │ └── internal │ │ └── ZipCodeValidatorImpl.java │ └── module-info.java ├── de.codecentric.zipvalidator.model │ ├── de │ │ └── codecentric │ │ └── zipvalidator │ │ └── model │ │ └── api │ │ └── ZipCodeValidationResult.java │ └── module-info.java
Класс ZipCodeValidationResult
– простое перечисление, имеющее экземпляры вида “too short”, “too long” и т.д.
Класс module-info.java
наследуется таким образом:
module de.codecentric.zipvalidator
Теперь наша реализация ZipCodeValidator выглядит так:
@Override public ZipCodeValidationResult zipCodeIsValid(String zipCode) < if (zipCode == null) < return ZipCodeValidationResult.ZIP_CODE_NULL; [snip] >else < return ZipCodeValidationResult.OK; >>
Модуль addresschecker теперь адаптирован так, что может принимать в качестве возвращаемого типа и перечисление, так что можно приступать, верно? Нет! Компиляция дает:
./de.codecentric.addresschecker/de/[..]/internal/AddressCheckerImpl.java:5: error: ZipCodeValidationResult is not visible because package de.codecentric.zipvalidator.model.api is not visible
Произошла ошибка при компиляции addresschecker – zipvalidator использует экспортированные типы из модели zipvalidator model в своем публичном API. Поскольку addresschecker не читает этот модуль, он не может обратиться к этому типу.
Существует два решения такой проблемы. Очевидное: добавить ребро чтения из addresschecker к модели zipvalidator. Однако это скользкая дорожка: зачем нам объявлять эту зависимость, если она нужна только для работы с zipvalidator? Разве zipvalidator не должен гарантировать, что мы сможем получить доступ ко всем необходимым модулям? Должен и может – здесь мы подходим к подразумеваемой читаемости. Добавив ключевое слово public к требуемому определению, мы сообщаем всем клиентским модулям, что они также должны считывать другой модуль. В качестве примера рассмотрим обновленный класс module-info.java
zipvalidator’а:
module de.codecentric.zipvalidator
Ключевое слово public
сообщает всем модулям, читающим zipvalidator, что они также должны читать его модель. Работать с путем к классам приходилось иначе: так, вы не могли положиться на Maven POM, если требовалось гарантировать, чтобы все ваши зависимости также были доступны любому клиенту; чтобы добиться этого, приходилось явно указывать их, если они входили в состав вашего публичного API. Это очень красивая модель: если вы используете зависимости только внутри класса, то какое дело до них вашим клиентам? А если используете их вне класса, то также должны прямо об этом сообщить.
Вот и подошла к концу первая часть. Мы обсудили три вопроса, на которые нужно ответить для каждого модуля, а также о модуляризации исполняющей среды Java. Далее мы рассмотрели пример, где скомпилировали, запустили и упаковали простое приложение на Java, состоящее из двух модулей. Затем на рабочем примере изучили, как система модулей реагирует на нарушение установленных правил. Далее, расширив функционал, мы изучили третий модуль и поговорили о концепции подразумеваемой читаемости.
В следующей части будут рассмотрены следующие вопросы:
- Как действует Jigsaw, если путь к модулям содержит несколько одноименных модулей?
- Что происходит, если в пути к модулям есть разноименные модули, которые, однако, экспортируют одни и те же пакеты?
- Что делать с унаследованными зависимостями, которые не модуляризованы?
- Как создать собственный урезанный вариант исполняющей среды?
- Блог компании Издательский дом «Питер»
- Программирование
- Java
Руководство по Java 9. Модульность.
Основным нововведением Java 9 было именно введение модульности. Это означает, что у нас появился новый компонент под названием модуль.
Модуль включает в себя код, данные и имя, по которому мы можем его идентифицировать.
Для того чтобы создать модуль, нам необходимо выполнить следующие шаги:
Создать проект в нужной директории. В моём случае это выглядит следующим образом:
Programming/Proselyte/Projects/Tutorials/Java9
src/net.proselyte.module
Внутри которой, необходимо создать файл module-info.java со следующим содержанием:
module net.proselyte.module <>
Создать класс Main:
public class Main < public static void main(String[] args) < System.out.println("Module TEST. "); >>

Зайти в директорию проекта и создать пакет с именем mods.
Внутри этого проекта создаём директорию с именем нашего модуля.
В результате мы должны получить проект со следующей структурой:
Шаг 5
Компилируем модуль в директорию mods:
javac -d mods/net.proselyte.module/ src/net.proselyte.module/module-info.java src/net.proselyte.module/net/proselyte/module/Main.java
Шаг 6:
Запускаем модуль:
java --module-path mods -m net.proselyte.module/net.proselyte.module.Main
В результате выполнения данной команды мы должны получить следующее:
Module test.
На этом мы заканчиваем разбор модульности в Java 9.
В следующей статье мы рассмотрим приватные методы в интерфейсах.