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

https://blog.codecentric.de/en/2015/12/first-steps-with-java9-jigsaw-part-2/
  • Перевод
Здравствуйте, Хабр.

После некоторого промедления публикуем вторую часть статьи о проекте Jigsaw и Java 9, вышедшую в блоге Codecentric. Перевод первой части находится здесь.


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

Здесь мы постараемся ответить на следующие вопросы:

  • Можно ли ввести ограничение на то, какие модули смогут читать экспортированный пакет?
  • Что делать с различными версиями одного и того же модуля, присутствующими в пути к модулям?
  • Как Jigsaw взаимодействует с немодульным унаследованным кодом?
  • Как собрать собственный образ исполняющей среды Java?


Возьмем за основу пример из части 1 и продолжим работать с ним. Код по-прежнему находится здесь.

Предоставление права на чтение конкретным модулям

В первой части мы говорили о том, как развивается доступность Java в рамках Jigsaw. Один из уровней доступности, который был упомянут, но не разъяснен как следует, таков: “публичный для некоторых модулей, тех, что читают этот модуль”. Так мы можем ограничить круг модулей, которым будет разрешено читать наши экспортированные пакеты. Допустим, разработчики de.codecentric.zipvalidator
терпеть не могут разработчиков de.codecentric.nastymodule
, поэтому могут изменить свой module-info.java
вот так:

module de.codecentric.zipvalidator{

    exports de.codecentric.zipvalidator.api 
        to de.codecentric.addresschecker;
}


Таким образом, лишь addresschecker
может получить доступ к API zipvalidator
. Данное указание выполняется на уровне пакетов, поэтому ничто вам не мешает ограничить доступ для одних пакетов, в то же время предоставив полный доступ для других. Такая практика именуется «квалифицированный экспорт». Если модуль de.codecentric.nastymodule
попытается обратиться к любому типу из de.codecentric.zipvalidator.api
, то возникнет ошибка компиляции:

./de.cc.nastymodule/de/cc/nastymodule/internal/AddressCheckerImpl.java:4: 
error: ZipCodeValidatorFactory is not visible 
       because package de.cc.zipvalidator.api is not visible


Обратите внимание: программа не ругается на module-info.java
, так как zipvalidator
в принципе мог бы экспортировать видимые пакеты в nastymodule
. Например, квалифицированный экспорт можно применить, когда вы хотите модуляризовать внутреннюю структуру вашего приложения, но не хотите делиться экспортируемыми пакетами внутренних модулей с клиентами.

Конфликты между версиями модулей

Часто случается так, что через транзитивные зависимости в одно и то же приложение попадают различные версии библиотеки — то есть, один и тот же модуль может дважды фигурировать в пути к модулям. Сразу приходят на ум два сценария:

  • Модули доступны во время компиляции в различных каталогах или модульных jar, но имя у них все равно одинаковое
  • Различные версии одного и того же модуля являются разноименными


Давайте попробуем скомпилировать приложение по первому сценарию. Скопировали zipvalidator
:

two-modules-multiple-versions
├── de.codecentric.addresschecker
│   ├── de
│   │   └── codecentric
│   │       └── addresschecker
│   │           ├── api
│   │           │   ├── AddressChecker.java
│   │           │   └── Run.java
│   │           └── internal
│   │               └── AddressCheckerImpl.java
│   └── module-info.java
├── de.codecentric.zipvalidator.v1
│   ├── de
│   │   └── codecentric
│   │       └── zipvalidator
│   │           ├── api
│   │           │   ├── ZipCodeValidator.java
│   │           │   └── ZipCodeValidatorFactory.java
│   │           ├── internal
│   │           │   └── ZipCodeValidatorImpl.java
│   │           └── model
│   └── module-info.java
├── de.codecentric.zipvalidator.v2
│   ├── de
│   │   └── codecentric
│   │       └── zipvalidator
│   │           ├── api
│   │           │   ├── ZipCodeValidator.java
│   │           │   └── ZipCodeValidatorFactory.java
│   │           ├── internal
│   │           │   └── ZipCodeValidatorImpl.java
│   │           └── model
│   └── module-info.java


Дублирующиеся модули находятся в разных каталогах, но имя модуля остается неизменным. Как Jigsaw реагирует на это во время компиляции?

./de.codecentric.zipvalidator.v2/module-info.java:1: 
error: duplicate module: de.codecentric.zipvalidator


Итак, здесь нам легко не отделаться. Jigsaw выдает ошибку компиляции, когда в пути к модулям присутствуют два одноименных модуля.

Что насчет второго случая? Структура каталогов остается прежней, но теперь оба zipvalidator’а получают разные имена (de.codecentric.zipvalidator.v{1|2}
), и addresschecker читает оба этих имени.

module de.codecentric.addresschecker{
     exports de.codecentric.addresschecker.api;
     requires de.codecentric.zipvalidator.v1;
     requires de.codecentric.zipvalidator.v2;
}


Скорее всего, и здесь не скомпилируется? Читать два модуля, экспортирующих одни и те же пакеты? Оказывается, скомпилируется. Я сам удивился: компилятор распознает возникшую ситуацию, но ограничивается лишь такими предупреждениями:

./de.cc.zipvalidator.v1/de/codecentric/zipvalidator/api/ZipCodeValidator.java:1: 
warning: package exists in another module: de.codecentric.zipvalidator.v2


Разработчик с готовностью проигнорирует такое предупреждение и запустит приложение. Но Jigsaw явно не нравится то, что он увидит во время выполнения:

java.lang.module.ResolutionException: 
Modules de.codecentric.zipvalidator.v2 and de.codecentric.zipvalidator.v1 export 
package de.codecentric.zipvalidator.api to module de.codecentric.addresschecker


Мне это кажется малопонятным, по-моему, ошибку времени компиляции можно было бы оформить и поаккуратнее. Я поинтересовался в рассылке, почему был выбран именно такой вариант, но на момент написания статьи ответа еще не получил.

Автоматические модули и безымянный модуль

До сих пор мы работали в полностью модуляризованной среде. Но что делать в таких весьма вероятных случаях, когда придется иметь дело с немодульными Jar-файлами? Здесь вступают в игру автоматические модули и безымянный модуль.

Начнем с автоматических модулей. Автоматический модуль — это jar-файл, поставленный в modulepath. После того, как вы его туда запишете, можно будет ответить на три следующих вопроса об этом модуле:

В: Каково его имя?
О: это имя jar-файла. Если вы поставите в путь к модулям файл guava.jar, то получите автоматический модуль guava.

Это также означает, что вы не сможете использовать Jar прямо из репозитория Maven, так как guava-18.0 не является допустимым идентификатором Java.

В: Что он экспортирует?
О: Автоматический модуль экспортирует все свои пакеты. Итак, все публичные типы будут доступны любому модулю, читающему автоматический модуль.

В: Что он требует?
О: Автоматический модуль читает все (*all*) прочие доступные модули (включая безымянный, подробнее об этом ниже). Это важно! Из автоматического модуля можно получить доступ ко всем экспортированным типам любого модуля. Этот момент нигде не нужно указывать, он подразумевается.

Рассмотрим пример. Мы начинаем использовать com.google.common.base.Strings в zipvalidator-е. Чтобы разрешить такой доступ, мы должны определить ребро считывания для автоматического модуля Guava:

module de.codecentric.zipvalidator{
       exports de.codecentric.zipvalidator.api;
       requires public de.codecentric.zipvalidator.model;
       requires guava;

 }


Для компиляции потребуется указать файл guava.jar в пути к модулям (он находится в каталоге ../jars):

javac -d . -modulepath ../jars -modulesourcepath .  $(find . -name "*.java")


Все прекрасно компилируется и запускается.

(Между прочим, было не так просто запустить эту пример. Работая со сборкой Jigsaw 86, я столкнулся с некоторыми проблемами: система ругалась по поводу зависимостей от модуля jdk.management.resource
. Я спросил об этом в рассылке, обсуждение находится здесь.

Нужно сказать, что в моем решении я не пользовался «ранней» сборкой (early access build), а собирал JDK сам. При работе с OSX Mavericks возникли еще некоторые проблемы, о чем написано в треде, пришлось изменить makefile, но в итоге я все наладил. Возможно, вам при работе со следующими релизами придется столкнуться уже с другими проблемами).

Теперь самое время познакомить вас с палочкой-выручалочкой, которая незаменима при переходе на Jigsaw. Этот инструмент называется jdeps
. Он просматривает ваш немодуляризованный код и сообщает вам о зависимостях.

Рассмотрим guava:

jdeps -s ../jars/guava.jar
Имеем такой вывод:
guava.jar -> java.base
guava.jar -> java.logging
guava.jar -> not found

Это означает, что автоматический модуль guava требует java.base
, java.logging
и … “не найдено“?! Что такое? Если убрать переключатель -s
, то jdeps
уходит с уровня модулей и спускается на шаг вниз, на уровень пакетов (список немного сокращен, так как пакетов у guava довольно много):

   com.google.common.xml (guava.jar)
      -> com.google.common.escape               guava.jar
      -> java.lang
      -> javax.annotation                                   not found


Здесь видно, что пакет com.google.common.xml
зависит от com.google.common.escape
, который расположен в самом модуле, java.lang
, который хорошо известен, и от аннотации javax.annotation
, которая не найдена. Делаем вывод, что нам нужен jar с типами JSR-305, поскольку там содержится javax.annotation
(кстати, я без них обхожусь – в моих примерах мне не требуется ни один тип из этих пакетов, и ни компилятор, ни исполняющая среда при этом не возражают).

Безымянный модуль

Итак, что же такое безымянный модуль? Вновь ответим на три вопроса:

В: Какого его имя?
О: Как вы уже догадались, имени у него нет

В: Что он экспортирует?
О: Безымянный модуль экспортирует все свои пакеты любому другому модулю. Это не означает, что его можно читать из любого другого модуля – у него нет имени, и вы не можете его требовать! Команда requires unnamed; не сработает.

В: Что он требует?
О: Безымянный модуль читает все прочие доступные модули.

Итак, если вы не можете прочитать безымянный модуль из любых ваших модулей, то в чем же суть? На этот вопрос помогает ответить наш старый знакомый — путь к классам. Любой тип, считанный из пути к классам (а не из пути к модулям), автоматически помещается в безымянном модуле — или, иными словами, любой тип в безымянном модуле загружается через путь к классам. Поскольку безымянный модуль читает все другие модули, мы можем обратиться ко всем экспортированным типам из любого типа, загруженного через путь к классам. В Java 9 использование пути к классам и пути к модулям будет поддерживаться как по отдельности, так и совместно, для обеспечения обратной совместимости. Рассмотрим несколько примеров.

Предположим, у нас есть аккуратный модуль zipvalidator, но addresschecker по-прежнему не модуляризован, у него нет module-info.java
. Структура наших исходников будет такой:

one-module-with-unnamed-ok/
├── classpath
│   └── de.codecentric.legacy.addresschecker
│       └── de
│           └── codecentric
│               └── legacy
│                   └── addresschecker
│                       ├── api
│                       │   ├── AddressChecker.java
│                       │   └── Run.java
│                       └── internal
│                           └── AddressCheckerImpl.java
├── modulepath
│   └── de.codecentric.zipvalidator
│       ├── de
│       │   └── codecentric
│       │       └── zipvalidator
│       │           ├── api
│       │           │   ├── ZipCodeValidator.java
│       │           │   └── ZipCodeValidatorFactory.java
│       │           └── internal
│       │               └── ZipCodeValidatorImpl.java
│       └── module-info.java


Теперь есть один каталог classpath, в котором содержится унаследованный код, завязанный на доступ к zipvalidator, а также каталог modulepath, содержащий модуль zipvalidator. Мы можем компилировать наши модули как обычно. Чтобы скомпилировать унаследованный код, нам потребуется предоставить информацию о модульном коде. Просто запишем ее в путь к классам:

javac -d classpath/de.codecentric.legacy.addresschecker  
  -classpath modulepath/de.codecentric.zipvalidator/ $(find classpath -name "*.java")


Все работает как обычно.

Во время исполнения перед нами открывается две возможности. А именно:
  • Записать модуль в путь к классам
  • Смешать путь к классам и путь к модулям


Выбирая первый вариант, мы, фактически, отказываемся от модульной системы. Все типы, которые мы запишем в безымянный модуль, теперь смогут свободно обращаться друг к другу.

java -cp modulepath/de.cc.zipvalidator/:classpath/de.cc.legacy.addresschecker/
    de.codecentric.legacy.addresschecker.api.Run 76185


работает точно как java-приложение, используемое вами сегодня.

С другой стороны, смешанное использование пути к модулям и пути к классам работает так:

java -modulepath modulepath  -addmods de.codecentric.zipvalidator 
    -classpath classpath/de.codecentric.legacy.addresschecker/ 
    de.codecentric.legacy.addresschecker.api.Run


Используем одновременно два переключателя: -classpath
и -modulepath
. Добавлен переключатель -addmods
– при смешивании пути к классам и пути к модулям, мы не можем просто так получить доступ к любому модулю в каталогах modulepath, а должны конкретно указать, какие из них должны быть доступны.

Этот подход также работает нормально, но здесь есть загвоздка! Помните, ответ на вопрос “чего требует безымянный модуль” — это “все другие модули”. Если мы будем использовать модуль zipvalidator через modulepath, то сможем работать лишь с его экспортированными пакетами. Все остальное приведет к IllegalAccessError во время исполнения. Поэтому в таком случае вам придется придерживаться правил системы модулей.

Создание образов исполняющей среды при помощи jlink

Достаточно примеров с модулями; появился еще один новый инструмент, заслуживающий нашего внимания. jlink
– это утилита Java 9 для создания собственных дистрибутивов JVM. Самое интересное, что, благодаря новой модульной архитектуре JDK, вы можете сами выбирать, какие модули хотите включить в этот дистрибутив! Рассмотрим пример. Если мы хотим создать образ исполняющей среды, содержащий наш addresschecker, то даем команду:

jlink --modulepath $JAVA9_BIN/../../images/jmods/:two-modules-ok/ 
    --addmods de.codecentric.addresschecker --output linkedjdk


Указываем всего три вещи:

  • Путь к модулям (в том числе, наши специальные модули и путь к каталогу jmods в вашем JDK – здесь находятся стандартные модули java)
  • Модули, которые вы хотите включить в ваш дистрибутив
  • Каталог вывода


Команда создает следующую структуру:

linkedjdk/
├── bin
│ ├── java
│ └── keytool
├── conf
│ ├── net.properties
│ └── security
│ ├── java.policy
│ └── java.security
└── lib
├── classlist
├── jli
    │   └── libjli.dylib
    ├── jspawnhelper
    ├── jvm.cfg
    ├── libjava.dylib
    ├── libjimage.dylib
    ├── libjsig.diz
    ├── libjsig.dylib
    ├── libnet.dylib
    ├── libnio.dylib
    ├── libosxsecurity.dylib
    ├── libverify.dylib
    ├── libzip.dylib
    ├── modules
    │   └── bootmodules.jimage
    ├── security
    │   ├── US_export_policy.jar
    │   ├── blacklisted.certs
    │   ├── cacerts
    │   └── local_policy.jar
    ├── server
    │   ├── Xusage.txt
    │   ├── libjsig.diz
    │   ├── libjsig.dylib
    │   ├── libjvm.diz
    │   └── libjvm.dylib
    └── tzdb.dat


Вот и все. В OSX Mavericks все это занимает примерно 47 MB. Мы также можем задействовать архивацию и удалить некоторые отладочные возможности, которые все равно не понадобятся нам в продакшене. Самый компактный дистрибутив, который мне удалось создать, получился при помощи такой команды:


jlink --modulepath $JAVA9_BIN/../../images/jmods/:two-modules-ok/bin 
    --addmods de.codecentric.addresschecker --output linkedjdk --exclude-files *.diz 
    --compress-resources on --strip-java-debug on --compress-resources-level 2


Размер дистрибутива уменьшается примерно до 18 MB, по-моему – просто великолепно. В Linux, вероятно, можно ужаться и до 13.

При вызове

/bin/java --listmods


Выводится список модулей, содержащихся в этом дистрибутиве

de.codecentric.addresschecker
de.codecentric.zipvalidator
java.base@9.0


Итак, все приложения, зависящие от максимального количества этих модулей, могут работать на данной JVM. Но мне не удалось получить наш основной класс для запуска этого сценария. Для этого я пошел другим путем.

Внимательный читатель мог заметить, что второй вызов делается к jlink, и путь к модулям там иной, нежели при первом вызове. Во втором случае мы указываем путь к каталогу bin. Этот каталог содержит модульные jar-файлы, и jar для addresschecker также содержит в своем манифесте информацию об основном классе. Утилита jlink использует эту информацию, чтобы добавить дополнительную информацию в bin-каталог нашей JVM:

linkedjdk/
├── bin
│   ├── de.codecentric.addresschecker
│   ├── java
│   └── keytool
...


Итак, теперь мы можем вызывать наше приложение напрямую. Красота!

./linkedjdk/bin/de.codecentric.addresschecker 76185


выводит

76185 is a valid zip code


Заключение

Вот и подошло к концу наше знакомство с Jigsaw. Мы рассмотрели ряд примеров, иллюстрирующих, что можно и чего нельзя сделать при помощи Jigsaw и Java 9. Jigsaw привносит коренные изменения, которые нельзя так запросто компенсировать при помощи лямбда-выражений или ресурсов try-with
. Весь наш тулчейн от сборочных инструментов вроде Maven или Gradle до IDE придется адаптировать к модульной системе. На конференции JavaOne Ханс Доктер из Gradle Inc. прочитал доклад о том, как можно приступить к написанию модульного кода даже на Java 8 и ниже. Gradle выполняет проверку во время компиляции и выдает отказ, если целостность модуля оказывается нарушена. Эта (экспериментальная) возможность была включена в последний релиз Gradle 2.9. Нас определенно ждут интересные времена!

Для более подробного знакомства с Jigsaw вновь рекомендую вам домашнюю страницу Jigsaw Project, особенно слайды и видео докладов о Jigsaw с последней конференции JavaOne.
Метки:
  • +14
  • 13,2k
  • 7
Поделиться публикацией
Похожие публикации
Комментарии 7
  • 0
    А можете осветить вопрос AOT?
    Судя по http://openjdk.java.net/projects/jigsaw/goals-reqs/03 он должен быть там
    • +2
      По-моему не взлетит. Основная необходимость модульной системы — решать конфликт с версиями транзитивных зависимостей — не решена. Требует радикальных изменений систем сборки. Также затронет существующие фреймворки, сервера приложений, и многие API, которые изначально не рассчитывались на модульную систему. Все ради сомнительной абстрактной цели.
      • 0
        А что на счёт производительности? Разве модульность не позволит сэкономить пару наносекунд, критичных для бирж?
        • 0
          jurikolo
          Как она должна помочь сэкономить пару наносекунд?

          Throwable
          Насколько я понял, как раз существующие фреймворки и системы сборки это не затронет. Цель то хорошая, это намного упростит ребятам жизнь в плане и безопасности и построения api, и самое главное должно упростить ребятам работу с aot
          • +2
            Все-таки, вопросы из первой части остаются открытыми.
            По поводу API: если мой модуль импортирует java.xml.bind, и я хочу инициализировать контекст с моими классами: JAXBContext jc = JAXBContext.newInstance(«com.acme.foo»), то классы com.acme.foo должны быть доступны для java.xml.bind. В предлагаемой схеме, неясно: либо java.xml.bind нужно где-то указать, что ему доступен модуль com.acme.foo для чтения. Плюс мой модуль обязательно должен экспортировать пакет com.acme.foo, чтобы он был видимым для java.xml.bind. Либо вообще юзать java.xml.bind в «безымянном» модуле, но тогда смысла в модулях я не вижу.
            • 0
              Они должны быть доступны для Thread.currentThread().getContextClassLoader() и что-то мне подсказывает, что это будет classloader вашего текущего модуля, в котором вы и пишете в зависимости java.xml.bind. Утверждать не буду, но сегодня займусь расследование)
      • +1
        По-моему херня какая-то (

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

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