Функция, представленная начиная с Java 9

В этой статье я хочу рассказать вам все, что вам нужно знать о модулях Java. Я намерен помочь разработчикам Java, которым можно задать вопрос на собеседовании, или тем, кто хочет получить сертификат Java 11 OCP. Если у вас есть причины читать о модулях, это тоже хорошо! Не стесняйтесь оставлять комментарии, если это так, потому что я был бы рад услышать от вас.

Будет больше, чем просто теория (и я упустил более сложные вещи, такие как jdeps), так что не бойтесь быть ошеломленным!

Что такое модуль?

Модуль - это программный компонент Java, представленный в Java 9. До Java 9 классы находились в пакетах, а классы внутри этих пакетов были найдены загрузчиками классов. Начиная с Java 9 модули действуют как промежуточные звенья между пакетами и загрузчиками классов. Ресурсы и пакеты (и классы в этих пакетах) могут быть инкапсулированы из других модулей на уровнях, которые раньше были просто невозможны. Это выводит защитную разработку на Java на более высокий уровень. Как разработчику это приятно слышать!

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

Модуль - это многоразовая группа связанных пакетов, ресурсов и дескриптора модуля с уникальным именем, которая определяет следующее:

  • имя
  • зависимости
  • экспортированные пакеты
  • предоставленные пакеты
  • использованные услуги
  • какие пакеты открыты

Имя

Объявление модуля определяет новый именованный модуль. Приведу несколько примеров корректных объявлений модулей:

module example {
}

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

Имя модуля состоит из одного или нескольких идентификаторов Java, разделенных знаком «.» жетоны. Это дает нам:

module continuum.be.example {
}

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

Зависимости

С одной стороны, именованный модуль определяет зависимости от других модулей, чтобы определить совокупность классов и интерфейсов, доступных для его кода. Зависимость - это то, что выражается директивой requires. В следующем объявлении модуля модуль continuum.be.example имеет прямую зависимость от модуля continuum.be.accounting.

module continuum.be.example {
requires continuum.be.accounting;
}

Также существует статическая директива requires, указывающая, что модуль требуется во время компиляции, но не является обязательным во время выполнения. Это называется необязательной зависимостью. Наконец, вы также можете использовать ключевое слово transitive, чтобы убедиться, что другие модули, читающие ваш модуль, также читают эту зависимость, известную как подразумеваемая читаемость.

Экспортированные пакеты

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

Экспортированные пакеты объявляются в дескрипторе модуля. Вот пример простой экспортной декларации:

module continuum.be.accounting {
exports continuum.be.accounting;
}

Подпакеты экспортированного пакета не экспортируются. Это означает, что если пакет учета содержал подпакет с именем util, то пакет com.continuum.accounting.util не экспортируется только потому, что com.continuum.accounting. Вам также не нужно экспортировать родительский пакет, если вы этого не хотите.

Предоставляемые пакеты

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

  • Требуется другой модуль, в данном случае com.continuum.service.
  • Реализуйте служебный интерфейс OurService с помощью класса Java OurServiceImpl.
  • Объявите реализацию интерфейса службы с помощью нотации предоставляет ‹interface› с ‹классом реализации›.
module com.continuum.implementation {
requires com.continuum.service;
provides com.continuum.service.OurService
with com.continuum.service.OurServiceImpl
}

Используемые услуги

Если у вас есть модуль интерфейса службы (например, JPA) и модуль реализации службы (например, Hibernate), вы можете создать клиентский модуль, который использует службу. Чтобы использовать службу по модульному принципу, вы должны require интерфейсный модуль, например com.continuum.service и заявите, что вы будете использовать его, используя ключевое слово uses. Примером этого может быть следующее:

module com.continuum.client {
requires com.continuum.service;
uses com.continuum.service.OurService;
}

Преимущество отсутствия объявления модуля реализации состоит в том, что модуль реализации может быть заменен без нарушения кода клиента. Вы можете решить, какую реализацию службы использовать при сборке приложения (например, указав версию в файле pom.xml при использовании maven). Это означает, что клиент и модуль интерфейса службы отделены от модуля реализации службы.

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

ServiceLoader<GenerationService> load = ServiceLoader.load(GenerationService.class);

Какие пакеты открыты

Ключевое слово opens определяет имя пакета, который будет открыт текущим модулем. Для кода в других модулях это предоставляет доступ во время выполнения (но не во время компиляции) к общедоступным и защищенным типам в пакете, а также к общедоступным и защищенным членам этих типов. Он также предоставляет рефлексивный доступ ко всем типам в пакете и всем их членам для кода в других модулях.

Также можно открыть его только для одного или нескольких модулей, используя opens … to ключевые слова. Настоятельно рекомендуется не использовать ключевое слово open для открытия всего модуля, но кто я такой, чтобы судить, чем вы занимаетесь в свободное время - независимо от того, что плавает ваша лодка!

Как насчет применения этой теории на практике?

Я поэкспериментировал с Service Loader API, maven и модулями и создал небольшое фитнес-приложение. Это показывает, насколько легко может быть создание модульного приложения, и это может быть хорошей отправной точкой для создания более сложных промышленных приложений.

Сначала, начиная с приложения Spring JPA, может быть сложно понять, что происходит в новой модульной системе. Вы можете получить приложение, которое даже не компилируется.

Это фитнес-приложение подчеркивает API загрузчика служб, который является менее известной альтернативой инъекции зависимостей. Для небольших приложений внедрение внедрения зависимостей из хорошо известного фреймворка может значительно увеличить размер приложения, что может быть плохо для программ, которым приходится беспокоиться об используемой памяти. Да, мы можем сделать это по-своему!

Модуль упражнений

Первый модуль нашего приложения - это модуль упражнений, интерфейсный модуль нашего поставщика услуг. Модуль упражнений экспортирует один пакет, com.continuum.generation.spi, и полностью не связан с реализациями. Если этот пакет изменится, он должен стать совместимым с интерфейсом, а не наоборот.

В этом пакете у нас есть GenerationService интерфейс, который позволяет нам создавать Schedule (например, график бега, график фитнеса) для определенного фитнеса Level , который является обычным перечислением.

Спортивный модуль

Второй модуль нашего фитнес-приложения - это модуль sports. Этот модуль предоставляет две реализации exercise spi. Поскольку они относятся к двум различным областям спорта, я поместил генераторы расписания running и fitness в отдельный пакет.

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

Это показывает, что вам не нужно помещать классы реализации в объявление модуля. Однако это, безусловно, возможно.

Главный модуль

Наконец, основным модулем будет наш клиентский модуль. Это требует exercise SPI и не имеет прямой зависимости от реализации. Мы также указываем, что собираемся использовать GenerationService. Предоставляемые реализации будут зависеть от тех, которые импортируются во время выполнения, что мы указываем в файле maven.

Нам нужно указать в нашем pom-файле как зависимость, которая предоставляет интерфейс, который мы собираемся использовать, то есть модуль упражнения, так и зависимость, которая предоставляет реализацию (и), в данном случае это модуль sports.

Если мы теперь запустим следующий класс в основном модуле:

Мы получаем следующие две строки вывода:

  • График занятий от ведущего специалиста Continuum Craft с уровнем: Продвинутый
  • График бега от мастера Continuum с уровнем: средний

Неплохо! Пользователь предоставленных модулей может работать с кодом, который теперь доступен во время выполнения, или сам GenerationService реализовать. Это также упрощает создание фиктивной реализации.

Что делать сейчас?

Разве это не здорово?

Есть больше видов спорта, чем просто бег и фитнес - почему бы вам не добавить график пив-понга?

Вы можете найти репо с более подробной информацией прямо здесь: https://github.com/djFooFoo/fitness. Не стесняйтесь экспериментировать с тем, как модули работают в Java.

Если чего-то не хватает или что-то, что можно было бы лучше объяснить, оставьте комментарий, и я свяжусь с вами. Спасибо за чтение!