Статьи

Project Jigsaw Практическое руководство

Project Jigsaw принесет модульность на платформу Java, и в соответствии с первоначальным планом он будет готов к завершению 10 декабря. Итак, мы здесь, но где Jigsaw?

Конечно, многое произошло за последние шесть месяцев: появился прототип , надвигающееся удаление внутренних API вызвало много шума , список рассылки полон критических обсуждений по поводу проектных решений проекта, а JavaOne провела серию замечательных вступительных докладов . команда Jigsaw А потом Java 9 задержалась на пол года из-за Jigsaw.

Но давайте пока проигнорируем все это и сосредоточимся на коде. В этом посте мы рассмотрим существующее демонстрационное приложение и модульно настроим его на Java 9. Если вы хотите продолжить, перейдите на GitHub , где можно найти весь код. Инструкции по установке важны для запуска сценариев с Java 9. Для краткости я удалил префикс org.codefx.demo из всех org.codefx.demo пакетов, модулей и папок в этой статье.

Приложение перед пазлом

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

  • Есть календарь, который имеет 24 календарных листа.
  • Каждый лист знает свой день месяца и содержит сюрприз.
  • Марш смерти на Рождество символизируется печатью листов (и, следовательно, сюрпризов) на консоли.

Конечно, календарь должен быть создан в первую очередь. Он может сделать это сам, но ему нужен способ создавать сюрпризы. С этой целью он получает список заводов-сюрпризов. Вот как выглядит main метод :

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    List<SurpriseFactory> surpriseFactories = Arrays.asList(
            new ChocolateFactory(),
            new QuoteFactory()
    );
    Calendar calendar =
        Calendar.createWithSurprises(surpriseFactories);
    System.out.println(calendar.asText());
}

Начальное состояние проекта ни в коем случае не лучшее из того, что возможно до Jigsaw. Наоборот, это упрощенная отправная точка. Он состоит из одного модуля (в абстрактном смысле, а не интерпретации Jigsaw), который содержит все необходимые типы :

  • «Surprise API» — Surprise и SurpriseFactory (оба являются интерфейсами)
  • «Calendar API» — Calendar и CalendarSheet для создания календаря
  • Сюрпризы — пара реализаций Surprise и SurpriseFactory
  • Главное — подключить и запустить все это.

Компиляция и запуск прямо (команды для Java 8):

1
2
3
4
5
6
# compile
javac -d classes/advent ${source files}
# package
jar -cfm jars/advent.jar ${manifest and compiled class files}
# run
java -jar jars/advent.jar

Вход в Пазл Ленд

Следующий шаг маленький, но важный. Он ничего не меняет в коде или его организации, но перемещает его в модуль Jigsaw.

Модули

Так что же это за модуль? Чтобы процитировать крайне рекомендуемое состояние системы модулей :

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

Чтобы управлять тем, как его код ссылается на типы в других модулях, модуль объявляет, какие другие модули ему требуются для компиляции и запуска. Чтобы контролировать, как код в других модулях ссылается на типы в своих пакетах, модуль объявляет, какой из этих пакетов он экспортирует .

Таким образом, по сравнению с JAR модуль имеет имя, которое распознается JVM, объявляет, от каких других модулей он зависит, и определяет, какие пакеты являются частью его открытого API.

название

Имя модуля может быть произвольным. Но для обеспечения уникальности рекомендуется придерживаться схемы именования пакетов с обратным URL. Поэтому, хотя в этом нет необходимости, это часто означает, что имя модуля является префиксом пакетов, которые он содержит.

зависимости

Модуль перечисляет другие модули, от которых зависит его компиляция и запуск. Это верно для модулей приложения и библиотеки, но также и для модулей в самом JDK, который был разделен примерно на 80 из них (посмотрите на них с помощью java -listmods ).

Опять из обзора дизайна:

Когда один модуль напрямую зависит от другого в графе модулей, тогда код в первом модуле сможет ссылаться на типы во втором модуле. Поэтому мы говорим, что первый модуль читает второй или, что то же самое, что второй модуль читается первым.

[…]

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

Когда любое из свойств нарушается, система модулей отказывается компилировать или запускать код. Это огромное улучшение по сравнению с хрупким путем к классам, где, например, отсутствующие JAR-файлы будут обнаружены только во время выполнения, что приведет к сбою приложения.

Стоит также отметить, что модуль может получать доступ к типам других только в том случае, если это напрямую зависит от него. Таким образом, если A зависит от B , который зависит от C , то A не сможет получить доступ к C, если это явно не требуется.

экспорт

Модуль перечисляет пакеты, которые он экспортирует. Только открытые типы в этих пакетах доступны снаружи модуля.

Это означает, что public больше не является публичной. Открытый тип в неэкспортированном пакете настолько же скрыт от внешнего мира, насколько и непубличный тип в экспортированном пакете. Который сегодня еще более скрыт, чем закрытые типы пакетов, потому что модульная система даже не обеспечивает рефлексивного доступа к ним. Поскольку Jigsaw в настоящее время реализован, флаги командной строки — единственный способ обойти это.

Реализация

Чтобы создать модуль, проекту нужен module-info.java в корневом каталоге с исходным кодом:

1
2
3
module advent {
    // no imports or exports
}

Подождите, разве я не говорил, что мы должны также объявлять зависимости от модулей JDK? Так почему мы ничего не упомянули здесь? Весь код Java требует Object и этот класс, а также несколько других, которые использует демонстрационная версия, являются частью модуля java.base . Таким образом, буквально каждый Java-модуль зависит от java.base , что привело команду Jigsaw к решению автоматически требовать его. Таким образом, мы не должны упоминать это явно.

Самое большое изменение — это скрипт для компиляции и запуска (команды для Java 9):

1
2
3
4
5
6
7
8
9
# compile (include module-info.java)
javac -d classes/advent ${source files}
# package (add module-info.class and specify main class)
jar -c \
    --file=mods/advent.jar \
    --main-class=advent.Main \
    ${compiled class files}
# run (specify a module path and simply name to module to run)
java -mp mods -m advent

Мы видим, что компиляция практически одинакова — нам нужно только включить новый module-info.java в список классов.

Команда jar создаст так называемый модульный JAR, то есть JAR, который содержит модуль. В отличие от того, что раньше, нам больше не нужен манифест, но мы можем указать основной класс напрямую Обратите внимание, как JAR создается в каталогах mods .

Совершенно другой способ запуска приложения. Идея состоит в том, чтобы сообщить Java, где найти модули приложения (с -mp mods , это называется путем к модулю ) и какой модуль мы хотели бы запустить (с -m advent ).

Разделение на модули

Теперь пришло время по-настоящему познакомиться с Jigsaw и разбить этот монолит на отдельные модули.

Выдуманное обоснование

«Удивительный API», то есть Surprise и SurpriseFactory , имеет большой успех, и мы хотим отделить его от монолита.

Заводы, которые создают сюрпризы, оказываются очень динамичными. Здесь проделана большая работа, они часто меняются, и какие фабрики используются, зависит от выпуска к выпуску. Поэтому мы хотим изолировать их.

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

Мы заканчиваем с этими модулями:

  • сюрпризSurprise и SurpriseFactory
  • календарь — календарь, который использует сюрприз API
  • фабрики — реализации SurpriseFactory
  • main — оригинальное приложение, теперь выдолбленное в классе Main

Глядя на их зависимости, мы видим, что сюрприз не зависит ни от какого другого модуля. И календарь, и фабрики используют его типы, поэтому они должны зависеть от него. Наконец, main использует фабрики для создания календаря, поэтому он зависит от обоих.

Fig1_50066

Реализация

Первым шагом является реорганизация исходного кода. Мы будем придерживаться структуры каталогов, как это предложено в официальном руководстве по быстрому запуску, и разместим все наши модули в своих собственных папках под src :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
src
  - advent.calendar: the "calendar" module
      - org ...
      module-info.java
  - advent.factories: the "factories" module
      - org ...
      module-info.java
  - advent.surprise: the "surprise" module
      - org ...
      module-info.java
  - advent: the "main" module
      - org ...
      module-info.java
.gitignore
compileAndRun.sh
LICENSE
README

Для удобства чтения я обрезал папки ниже org . Чего не хватает, так это пакетов и, в конечном итоге, исходных файлов для каждого модуля. Смотрите его на GitHub во всей красе.

Давайте теперь посмотрим, что должна содержать информация о модуле и как мы можем скомпилировать и запустить приложение.

сюрприз

Нет обязательных предложений, так как неожиданность не имеет зависимостей. (За исключением java.base , который всегда неявно требуется.) Он экспортирует пакет advent.surprise поскольку он содержит два класса Surprise и SurpriseFactory .

Итак, module-info.java выглядит следующим образом:

1
2
3
4
5
module advent.surprise {
    // requires no other modules
    // publicly accessible packages
    exports advent.surprise;
}

Компиляция и упаковка очень похожи на предыдущий раздел. На самом деле это даже проще, потому что сюрпризы не содержат основной класс:

1
2
3
4
# compile
javac -d classes/advent.surprise ${source files}
# package
jar -c --file=mods/advent.surprise.jar ${compiled class files}

календарь

В календаре используются типы из неожиданного API, поэтому модуль должен зависеть от неожиданности . Добавление requires advent.surprise для модуля достигает этого.

API модуля состоит из класса Calendar . Для того, чтобы он был общедоступным, содержащийся в нем пакет advent.calendar должен быть экспортирован. Обратите внимание, что CalendarSheet , принадлежащий тому же пакету, не будет виден за пределами модуля.

Но есть еще одна особенность: мы только что сделали Calendar.createWithSurprises( List<SurpriseFactory> ) общедоступным, что предоставляет доступ к типам из модуля неожиданности . Поэтому, если модули, читающие календарь, также не требуют неожиданности , Jigsaw не позволит им получить доступ к этим типам, что приведет к ошибкам компиляции и времени выполнения.

Пометка пункта require как public исправляет это. При этом любой модуль, который зависит от календаря, также читает сюрприз . Это называется подразумеваемой читабельностью .

Окончательная информация о модуле выглядит следующим образом:

1
2
3
4
5
6
module advent.calendar {
    // required modules
    requires public advent.surprise;
    // publicly accessible packages
    exports advent.calendar;
}

Компиляция почти такая же, как раньше, но, конечно, здесь должна быть отражена зависимость от неожиданности . Для этого достаточно указать компилятору mods каталогов, так как он содержит необходимый модуль:

1
2
3
4
5
6
7
8
# compile (point to folder with required modules)
javac -mp mods \
    -d classes/advent.calendar \
    ${source files}
# package
jar -c \
    --file=mods/advent.calendar.jar \
    ${compiled class files}

фабрики

Фабрики реализуют SurpriseFactory поэтому этот модуль должен зависеть от неожиданности . И поскольку они возвращают примеры Surprise из опубликованных методов, то же самое мышление, что и выше, приводит к requires public предложения.

Фабрики можно найти в пакете advent.factories чтобы их можно было экспортировать. Обратите внимание, что открытый класс AbstractSurpriseFactory , который находится в другом пакете, недоступен за пределами этого модуля.

Итак, мы получаем:

1
2
3
4
5
6
module advent.factories {
    // required modules
    requires public advent.surprise;
    // publicly accessible packages
    exports advent.factories;
}

Компиляция и упаковка аналогичны календарю .

главный

Наше приложение требует два модуля календаря и фабрики для компиляции и запуска. У него нет API для экспорта.

1
2
3
4
5
6
module advent {
    // required modules
    requires advent.calendar;
    requires advent.factories;
    // no exports
}

Компиляция и упаковка аналогичны работе с единственным модулем последнего раздела, за исключением того, что компилятору необходимо знать, где искать требуемые модули:

01
02
03
04
05
06
07
08
09
10
11
#compile
javac -mp mods \
    -d classes/advent \
    ${source files}
# package
jar -c \
    --file=mods/advent.jar \
    --main-class=advent.Main \
    ${compiled class files}
# run
java -mp mods -m advent

Сервисы

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

Выдуманное обоснование

Кто-то недавно прочитал пост в блоге о том, как круто слабое соединение. Затем она посмотрела на наш код сверху и пожаловалась на тесные отношения между основным и фабриками . Зачем вообще знать фабрики ?

Потому как…

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    List<SurpriseFactory> surpriseFactories = Arrays.asList(
            new ChocolateFactory(),
            new QuoteFactory()
    );
    Calendar calendar =
        Calendar.createWithSurprises(surpriseFactories);
    System.out.println(calendar.asText());
}

В самом деле? Просто для создания некоторых реализаций совершенно прекрасной абстракции ( SurpriseFactory )?

И мы знаем, что она права. Если кто-то другой предоставит нам реализации, это устранит прямую зависимость. Еще лучше, если указанный посредник сможет найти все реализации по пути модулей, сюрпризы календаря можно легко настроить, добавив или удалив модули перед запуском.

Это действительно возможно с Jigsaw. У нас может быть модуль, указывающий, что он обеспечивает реализации интерфейса. Другой модуль может выразить, что он использует упомянутый интерфейс и найти все реализации с ServiceLocator .

Мы используем эту возможность, чтобы разделить фабрики на шоколад и процитировать и получить следующие модули и зависимости:

  • сюрпризSurprise и SurpriseFactory
  • календарь — календарь, который использует сюрприз API
  • шоколадChocolateFactory как услуга
  • цитатаQuoteFactory как услуга
  • главное — приложение; больше не требует отдельных заводов

Fig2_50066

Реализация

Первым шагом является реорганизация исходного кода. Единственное изменение по src/advent.factories состоит в том, что src/advent.factories заменяется src/advent.factory.chocolate и src/advent.factory.quote .

Давайте посмотрим на отдельные модули.

сюрприз и календарь

Оба неизменны.

шоколад и цитата

Оба модуля идентичны, за исключением некоторых имен. Давайте посмотрим на шоколад, потому что он более вкусный.

Как и прежде, с фабриками модуль requires public модуля неожиданности

Более интересным является его экспорт. Он обеспечивает реализацию SurpriseFactory , а именно ChocolateFactory , которая указывается следующим образом:

1
2
provides advent.surprise.SurpriseFactory
    with advent.factory.chocolate.ChocolateFactory;

Поскольку этот класс является полным его открытым API, ему не нужно ничего экспортировать. Следовательно, никакое другое условие экспорта не требуется.

Мы заканчиваем с:

1
2
3
4
5
6
7
module advent.factory.chocolate {
    // list the required modules
    requires public advent.surprise;
    // specify which class provides which service
    provides advent.surprise.SurpriseFactory
        with advent.factory.chocolate.ChocolateFactory;
}

Компиляция и упаковка просты:

1
2
3
4
5
6
javac -mp mods \
    -d classes/advent.factory.chocolate \
    ${source files}
jar -c \
    --file mods/advent.factory.chocolate.jar \
    ${compiled class files}

главный

Самая интересная часть о main — это то, как он использует ServiceLocator, чтобы найти реализацию SurpriseFactory. Из его основного метода :

1
2
3
List surpriseFactories = new ArrayList<>();
ServiceLoader.load(SurpriseFactory.class)
    .forEach(surpriseFactories::add);

Наше приложение теперь требует только календарь, но должно указывать, что оно использует SurpriseFactory . У него нет API для экспорта.

1
2
3
4
5
6
7
module advent {
    // list the required modules
    requires advent.calendar;
    // list the used services
    uses advent.surprise.SurpriseFactory;
    // exports no functionality
}

Компиляция и выполнение как прежде.

И мы действительно можем изменить сюрпризы, которые календарь в конечном итоге будет содержать, просто удалив один из заводских модулей из пути модуля. Ухоженная!

Резюме

Ну это все. Мы видели, как перенести монолитное приложение в один модуль и как мы можем разделить его на несколько. Мы даже использовали локатор сервисов, чтобы отделить наше приложение от конкретных реализаций сервисов. Все это на GitHub, поэтому проверьте его, чтобы увидеть больше кода!

Но есть еще много о чем поговорить! Jigsaw приносит пару несовместимостей, а также средства для решения многих из них. И мы не говорили о том, как отражение взаимодействует с модульной системой и как переносить внешние зависимости.

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

Ссылка: Руководство по Jigsaw проекта от нашего партнера JCG Николая Парлога в блоге Java Advent Calendar .