Первые шаги с Java 9 и проект Jigsaw – часть первая

https://blog.codecentric.de/en/2015/11/first-steps-with-java9-jigsaw-part-1/
  • Перевод
Доброе утро, Хабр!

Еще со времен книги "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 изменяется с

  • public
  • private
  • default
  • protected


на

  • публичный для всех, кто читает этот модуль (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{
    exports de.codecentric.addresschecker.api;
    requires de.codecentric.zipvalidator;
}


Общая структура файловой системы такова:

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) {
    throw new  RuntimeException(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{
        requires de.codecentric.addresschecker;
        exports de.codecentric.zipvalidator.api;

}


Поскольку циклические зависимости запрещены по определению, на нашем пути (ради общего блага) встает компилятор:

./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{
       exports de.codecentric.zipvalidator.api;
       requires de.codecentric.zipvalidator.model;
}


Теперь наша реализация ZipCodeValidator выглядит так:

@Override
public <strong>ZipCodeValidationResult</strong> 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{
       exports de.codecentric.zipvalidator.api;
       requires public de.codecentric.zipvalidator.model;
}


Ключевое слово public
сообщает всем модулям, читающим zipvalidator, что они также должны читать его модель. Работать с путем к классам приходилось иначе: так, вы не могли положиться на Maven POM, если требовалось гарантировать, чтобы все ваши зависимости также были доступны любому клиенту; чтобы добиться этого, приходилось явно указывать их, если они входили в состав вашего публичного API. Это очень красивая модель: если вы используете зависимости только внутри класса, то какое дело до них вашим клиентам? А если используете их вне класса, то также должны прямо об этом сообщить.

Резюме

Вот и подошла к концу первая часть. Мы обсудили три вопроса, на которые нужно ответить для каждого модуля, а также о модуляризации исполняющей среды Java. Далее мы рассмотрели пример, где скомпилировали, запустили и упаковали простое приложение на Java, состоящее из двух модулей. Затем на рабочем примере изучили, как система модулей реагирует на нарушение установленных правил. Далее, расширив функционал, мы изучили третий модуль и поговорили о концепции подразумеваемой читаемости.

В следующей части будут рассмотрены следующие вопросы:

  • Как действует Jigsaw, если путь к модулям содержит несколько одноименных модулей?
  • Что происходит, если в пути к модулям есть разноименные модули, которые, однако, экспортируют одни и те же пакеты?
  • Что делать с унаследованными зависимостями, которые не модуляризованы?
  • Как создать собственный урезанный вариант исполняющей среды?
Метки:
  • +22
  • 31,8k
  • 8
Поделиться публикацией
Похожие публикации
Комментарии 8
  • +5
    Я правильно понимаю что модуляризованные JDK позволит в одном проекте (но разных модулях) использовать разные версии одной и той же библиотеке, то есть модуляризованные JDK это некий аналог OSGI и ей подобных технологий?
    • 0
      Class.forName(String className) will throw ClassNotFoundException if the Class object for the given string name cannot be found, which may be because the class, that is visible in classpath mode, is no longer visible in module mode. For example, if module B depends on (requires) module A then only the classes that module A exports are visible to classes in module B.

      openjdk.java.net/projects/jigsaw/doc/jdk-modularization-tips

      Как я понял, нужны разные имена для модулей. Если модуль один и тот же, но разные версии, то не выйдет ничего. Придется писать модуль-обертку.
      • +2
        Кажется, об этом автор собирается написать во второй части
      • 0
        То, чего давно не хватает.
      • +13
        Щас разберемся. Вопросы следующие:

        1. Рантайм грузит каждый модуль собственным classloader-ом, как OSGI? Или как раньше все в общем супе?
        2. Как обстоят дела с версионированием модулей и сожительством одинаковых классов разных версий?
        3. Что насчет экспорта/импорта ресурсов? ClassLoader.getResources() ищет только экспортнутые ресурсы по всем загруженным модулям, или только в текущем и импортнутых?
        4. Экспортирование транзитивно?
        5. Я хочу «наследовать» чей-то модуль, т.е. сделать свой модуль, добавив в него пару классов (или перекрыв старые). При этом, ессно, его приватная часть должна быть доступна. Можно ли это?
        5. И, наконец, как я понял, импортируемый модуль должен иметь доступ ко всем классам импортирующего. Иначе это нарушит работу кучи библиотек и API. Пример:
        JAXBContext jc = JAXBContext.newInstance( «com.acme.foo» );
        JAXBContext лежит модуле в java.xml.bind, но доменные классы в com.acme.foo. То есть классы из com.acme.foo должны быть доступны модулю java.xml.bind, даже если он не импортирует модуль com.acme.foo.

        6. И наконец, где и как вообще рантайм проверяет private/export class checking? Это фича исключительно classloader-а при резольвинге класса, или вообще JVM следит за тем, чтобы везде и всегда visibility не нарушалась?

        Например, я вызываю из своего модуля метод объекта из другого модуля:

        servicemodule.AnyService.anyMethod(params) throws AnyException

        Все хорошо, все export-нуто. Но внезапно AnyException.getCause() содержит внутренний эксепшн из другого модуля, который не импортирован. Если рантайм на это не выругается, то есть возможность получить ссылку на ClassLoader импортируемого модуля и загрузить любой внутренний класс: AnyException.getCause().getClass().getClassLoader().loadClass()
        • 0
          Коварные вопросы задаете :) Особенно про Exception.

          Я, ковыряя OSGI, пытался его сломать (была надобность сделать нечто, что там запрещено), мне не удалось это. Не возьму в толк, почему OSGI нельзя было взять за основу — он же отлажен отлично… Сложный, что ли?
        • 0
          4. Экспортирование транзитивно?

          Судя по разделу «Подразумеваемая считываемость», по умолчанию нет. Транзитивность включается использованием
          requires public <module>
          

          • 0
            Отличная вещь, но такое уже есть. Называется OSGI Bundles. Загружается с помощью библиотеки Equinox, содержащей в себе специальный ClassLoader.

            Технологии сто лет в обед, на ней построен Eclipse.

            Хотя нативная поддержка модульности в компиляторе — правильный путь развития…

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое