Файл package-info.java
В этом руководстве мы поймем назначение package-info.java и его полезность. Проще говоря, package-info — это файл Java, который можно добавить в любой пакет Java .
2. Назначение package-info
В настоящее время файл package-info.java служит двум целям:
- Место для документации на уровне пакетов
- Главная для аннотаций на уровне пакета
Помимо вышеупомянутого, варианты использования могут быть расширены по мере необходимости. В будущем, если потребуется добавить какую-либо функцию на уровне пакета, этот файл будет идеальным местом.
Давайте подробно рассмотрим текущие варианты использования.
3. Пакет документации
До версии Java 5 документация, относящаяся к пакету, помещалась в HTML-файл package.html . Это обычный файл HTML с комментариями Javadoc, размещенными внутри тега body .
Когда JDK 5 появился на сцене, package.html уступил место новому параметру package-info.java , который теперь предпочтительнее package.html .
Давайте посмотрим на пример документации пакета в файле package-info.java :
/** * This module is about impact of the final keyword on performance * * This module explores if there are any performance benefits from * using the final keyword in our code. This module examines the performance * implications of using final on a variable, method, and class level. * * * @since 1.0 * @author foreach * @version 1.1 */ package com.foreach.finalkeyword;
Приведенный выше package-info.java сгенерирует Javadoc:

Итак, так же, как мы пишем Javadoc в других местах, мы можем поместить пакет Javadoc в исходный файл Java.
4. Аннотации пакетов
Предположим, нам нужно применить аннотацию ко всему пакету . В этом случае нам может прийти на помощь package-info.java .
Рассмотрим ситуацию, когда нам нужно объявить поля, параметры и возвращаемые значения ненулевыми по умолчанию. Мы можем достичь этой цели, просто включив аннотацию @NonNullApi для ненулевых параметров и возвращаемых значений, ` а также аннотацию @NonNullFields для ненулевых полей в наш файл package-info.java .`
@NonNullFields и @NonNullApi будут помечать поля, параметры и возвращаемые значения как ненулевые, если они явно не помечены как @Nullable :
@NonNullApi @NonNullFields package com.foreach.nullibility; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields;
Существуют различные аннотации, которые можно использовать на уровне пакета. Например, в проекте Hibernate у нас есть категория аннотаций , а в проекте JAXB также есть аннотации уровня пакета .
5. Как создать файл с информацией о пакете
Создать файл с информацией о пакете довольно просто: мы можем создать его вручную или обратиться за помощью в IDE для его создания.
В IntelliJ IDEA мы можем щелкнуть правой кнопкой мыши пакет и выбрать New->package-info.java :

Опция Eclipse New Java Package позволяет нам сгенерировать package-info.java :
Вышеупомянутый метод работает и для существующих пакетов. Выберите существующий пакет, выберите опцию New-> Package и отметьте опцию Create package-info.java .
Хорошей практикой всегда является обязательное включение package-info.java в правила кодирования наших проектов. В этом могут помочь такие инструменты, как Sonar или Checkstyle .
6. Заключение
Основное различие между использованием файлов HTML и Java заключается в том, что с файлом Java у нас есть дополнительная возможность использования аннотаций Java. Таким образом , java- файл с информацией о пакете является не только домом для пакета Javadocs, но и аннотаций для всего пакета . Кроме того, этот список вариантов использования может быть расширен в будущем .
Как всегда, код доступен на GitHub .
- 1. Обзор
- 2. Назначение package-info
- 3. Пакет документации
- 4. Аннотации пакетов
- 5. Как создать файл с информацией о пакете
- 6. Заключение
Зачем нужен package-info?
Обычно, чтобы создать новый пакет, вы создаете папку с соответствующим названием, и во всех ее .java файлах добавляете в начале строчку с названием пакета, вида package full.dir.path; . Этого вполне достаточно для работы.
Однако, дополнительно вы можете добавить в пакет его объявление – файл package-info.java . Несмотря на то, что это .java-файл, это не объявление класса. В названии класса дефисы запрещены, так что неоднозначности не возникнет.
Минимальное содержимое файла package-info.java – всё та же строка package full.dir.path; . Такой файл не несет практической пользы. Есть две вещи, которые можно в него добавить, чтобы польза появилась.
Первое – документация пакета. Javadoc-комментарий будет использоваться генератором документации. Пример – описание пакета java.lang. Его исходник вы можете найти в файле java/lang/package-info.java .
Второе возможное применение – аннотации уровня пакета. Для этого аннотация должна иметь Target PACKAGE . Например, некоторые пакеты Spring Framework помечены аннотацией @NonNullApi .
Подробно объявления пакетов описаны в главе 7.4.1 спецификации.
Модульность в Java 9
Основным нововведением 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
module jdk.javadoc
После ключевого слова 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 java.sql.Driver;
provides javax.tools.Tool with jdk.javadoc.internal.api.JavadocTool;
Допустим, у нас подключено несколько модулей, которые реализуют абстрактный сервис — MyService. При сборке приложения у нас есть возможность решить, какую реализацию сервиса использовать, “перетащив” нужные нам модули реализации сервиса на —module-path:
Iterable 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 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 и так далее. Но большинство библиотек так и не перешло на полное использование модулей. Видимо поэтому, все эти изменения были восприняты довольно скептически техничесим сообществом. Модули предоставляют нам новые возможности, но вопрос востребованности остаётся открытым.
Ну и напоследок, предлагаю подборку материалов на эту тему:
Первые шаги с 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