Рубрики
Без рубрики

Руководство по модульности Java 9

Изучите основы создания и использования модулей в Java 9. Начните свой путь модульизации кода уже сегодня.

Автор оригинала: Christopher Franklin.

1. Обзор

Java 9 вводит новый уровень абстракции над пакетами, формально известный как система модулей платформы Java (JPMS), или сокращенно “Модули”.

В этом уроке мы рассмотрим новую систему и обсудим ее различные аспекты.

Мы также создадим простой проект, чтобы продемонстрировать все концепции, которые мы изучим в этом руководстве.

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

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

Модуль-это группа тесно связанных пакетов и ресурсов вместе с новым файлом дескриптора модуля.

Другими словами, это абстракция “пакет пакетов Java”, которая позволяет нам сделать наш код еще более многоразовым.

2.1. Пакеты

Пакеты внутри модуля идентичны пакетам Java, которые мы используем с момента создания Java.

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

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

2.2. Ресурсы

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

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

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

2.3. Дескриптор модуля

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

  • Имя – имя нашего модуля
  • Зависимости – список других модулей, от которых зависит данный модуль
  • Общедоступные пакеты – список всех пакетов, которые мы хотим получить из-за пределов модуля
  • Предлагаемые услуги – мы можем предоставить реализации услуг, которые могут быть использованы другими модулями
  • Потребляемые услуги – позволяет текущему модулю быть потребителем услуги
  • Разрешения на отражение – явно позволяет другим классам использовать отражение для доступа к закрытым членам пакета

Правила именования модулей аналогичны тому, как мы называем пакеты (точки разрешены, тире-нет). Очень часто используются имена стилей в стиле проекта (my.module) или обратного DNS ( com.baeldung.mymodule ). В этом руководстве мы будем использовать стиль проекта.

Нам нужно перечислить все пакеты, которые мы хотим сделать общедоступными, потому что по умолчанию все пакеты являются частными.

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

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

2.4. Типы модулей

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

  • Системные модули – Это модули, перечисленные при выполнении команды list-modules выше. Они включают в себя модули Java SE и JDK.
  • Прикладные модули – Эти модули мы обычно хотим создать, когда решаем использовать модули. Они названы и определены в скомпилированном module-info.class файл, включенный в собранную банку.
  • Автоматические модули – Мы можем включить неофициальные модули, добавив существующие файлы JAR в путь к модулю. Имя модуля будет производным от имени JAR. Автоматические модули будут иметь полный доступ для чтения ко всем другим модулям, загруженным по пути.
  • Безымянный модуль – Когда класс или JAR загружается в путь к классу, но не в путь к модулю, он автоматически добавляется в безымянный модуль. Это универсальный модуль для поддержания обратной совместимости с ранее написанным кодом Java.

2.5. Распределение

Модули могут распространяться одним из двух способов: в виде файла JAR или в виде “взорванного” скомпилированного проекта. Это, конечно, то же самое, что и любой другой проект Java, поэтому это не должно вызывать удивления.

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

Однако мы должны быть осторожны, потому что у нас может быть только один модуль на файл JAR.

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

3. Модули по Умолчанию

Когда мы устанавливаем Java 9, мы видим, что JDK теперь имеет новую структуру.

Они взяли все исходные пакеты и перенесли их в новую модульную систему.

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

java --list-modules

Эти модули разделены на четыре основные группы: java, javafx, jdk, и Oracle .

модули java являются классами реализации для спецификации языка core SE.

модули javafx – это библиотеки пользовательского интерфейса FX.

Все, что необходимо самому JDK, хранится в jdk модули.

И, наконец, все, что относится к Oracle, находится в модулях oracle .

4. Объявления модулей

Чтобы настроить модуль, нам нужно поместить специальный файл в корень наших пакетов с именем Чтобы настроить модуль, нам нужно поместить специальный файл в корень наших пакетов с именем .

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

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

module myModuleName {
    // all directives are optional
}

Мы начинаем объявление модуля с ключевого слова module , а затем следуем за ним с именем модуля.

Модуль будет работать с этим объявлением, но обычно нам потребуется дополнительная информация.

Вот тут-то и вступают в силу директивы модуля.

4.1. Требуется

Наша первая директива – требует . Эта директива модуля позволяет нам объявлять зависимости модулей:

module my.module {
    requires module.name;
}

Теперь my.module имеет как время выполнения, так и зависимость от времени компиляции от module.name .

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

4.2. Требуется Статическое

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

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

В этих случаях мы хотим использовать необязательную зависимость. Используя директиву requires static , мы создаем зависимость только во время компиляции:

module my.module {
    requires static module.name;
}

4.3. Требуется Переходный

Мы обычно работаем с библиотеками, чтобы облегчить себе жизнь.

Но мы должны убедиться, что любой модуль, который вводит наш код, также вводит эти дополнительные “транзитивные” зависимости, иначе они не будут работать.

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

module my.module {
    requires transitive module.name;
}

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

4.4. Экспорт

По умолчанию модуль не предоставляет какой-либо из своих API другим модулям. Эта сильная инкапсуляция была одним из ключевых мотиваторов для создания модульной системы в первую очередь.

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

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

module my.module {
    exports com.my.package.name;
}

Теперь, когда кто-то делает требует my.module , у него будет доступ к общедоступным типам в нашем com.my.package.name пакет, но не любой другой пакет.

4.5. Экспорт … В

Мы можем использовать exports…to чтобы открыть наши публичные классы для всего мира.

Но что, если мы не хотим, чтобы весь мир получил доступ к нашему API?

Мы можем ограничить, какие модули имеют доступ к нашим API, используя exports…to директива.

Аналогично директиве exports , мы объявляем пакет экспортированным. Но мы также перечисляем, какие модули мы разрешаем импортировать этот пакет в качестве требуется . Давайте посмотрим, как это выглядит:

module my.module {
    export com.my.package.name to com.specific.package;
}

4.6. Использование

/| service – это реализация определенного интерфейса или абстрактного класса, который может быть использован другими классами.

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

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

module my.module {
    uses class.name;
}

Здесь мы должны отметить, что существует разница между директивой requires и директивой uses .

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

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

4.7. Обеспечивает … С

Модуль также может быть поставщик услуг что другие модули могут потреблять.

Первая часть директивы-это ключевое слово provides . Здесь мы помещаем имя интерфейса или абстрактного класса.

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

Вот как это выглядит вместе взятое:

module my.module {
    provides MyInterface with MyInterfaceImpl;
}

4.8. Открыть

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

До Java 9 можно было использовать отражение для проверки каждого типа и члена пакета, даже private . Ничто не было действительно инкапсулировано, что может открыть всевозможные проблемы для разработчиков библиотек.

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

Если мы хотим продолжать разрешать полное отражение, как это делали более старые версии Java, мы можем просто открыть весь модуль:

open module my.module {
}

4.9. Открывает

Если нам нужно разрешить отражение частных типов, но мы не хотим, чтобы весь наш код был открыт, мы можем использовать директиву opens для предоставления определенных пакетов.

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

module my.module {
  opens com.my.package;
}

4.10. Открывается … Для

Ладно, иногда размышление-это здорово, но мы все равно хотим получить как можно больше безопасности от инкапсуляции . Мы можем выборочно открывать наши пакеты в предварительно утвержденном списке модулей, в этом случае используя opens…to директива :

module my.module {
    opens com.my.package to moduleOne, moduleTwo, etc.;
}

5. Параметры командной строки

К настоящему времени поддержка модулей Java 9 была добавлена в Maven и Gradle, так что вам не нужно будет много вручную создавать свои проекты. Тем не менее, по-прежнему полезно знать как использовать систему модулей из командной строки.

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

  • путь к модулю Мы используем параметр –путь к модулю для указания пути к модулю. Это список одного или нескольких каталогов, содержащих ваши модули.
  • add-reads – Вместо того, чтобы полагаться на файл объявления модуля, мы можем использовать эквивалент командной строки директивы requires ; –add-reads .
  • добавить-exports Замена командной строки для директивы exports .
  • добавить-открывает Замените предложение open в файле объявления модуля.
  • add-modules Добавляет список модулей в набор модулей по умолчанию
  • list-modules Выводит список всех модулей и строк их версий
  • patch-module – Добавление или переопределение классов в модулях
  • незаконный доступ=разрешить|предупредить|запретить – Либо ослабьте сильную инкапсуляцию, показав одно глобальное предупреждение, показав каждое предупреждение, либо завершите работу с ошибками. По умолчанию используется разрешение .

6. Видимость

Мы должны потратить немного времени на обсуждение видимости нашего кода.

Многие библиотеки зависят от отражения, чтобы творить свою магию (на ум приходят JUnit и Spring).

По умолчанию в Java 9 мы будем только иметь доступ к общедоступным классам, методам и полям в наших экспортированных пакетах. Даже если мы используем отражение, чтобы получить доступ к непубличным членам и вызвать setAccessible(true), мы не сможем получить доступ к этим членам.

Мы можем использовать open , opens и opens…to параметры предоставления доступа только во время выполнения для отражения. Обратите внимание, это только во время выполнения!

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

Если у нас должен быть доступ к модулю для отражения, и мы не являемся владельцем этого модуля (т. Е. Мы не можем использовать opens…to директива), то можно использовать опцию командной строки –add-opens , чтобы разрешить собственным модулям доступ к заблокированному модулю во время выполнения.

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

7. Собрать Все Это Воедино

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

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

7.1. Настройка Нашего Проекта

Во-первых, нам нужно настроить структуру нашего проекта. Мы создадим несколько каталогов для организации наших файлов.

Начните с создания папки проекта:

mkdir module-project
cd module-project

Это основа всего нашего проекта, поэтому добавьте сюда файлы, такие как файлы сборки Maven или Gradle, другие исходные каталоги и ресурсы.

Мы также поместили каталог для хранения всех модулей нашего проекта.

Далее мы создаем каталог модулей:

mkdir simple-modules

Вот как будет выглядеть структура нашего проекта:

module-project
|- // src if we use the default package
|- // build files also go at this level
|- simple-modules
  |- hello.modules
    |- com
      |- baeldung
        |- modules
          |- hello
  |- main.app
    |- com
      |- baeldung
        |- modules
          |- main

7.2. Наш Первый Модуль

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

В каталоге simple-modules создайте новый каталог с именем hello.modules .

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

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

com.baeldung.modules.hello

Затем создайте новый класс с именем HelloModules.java в этом пакете. Мы сохраним код простым:

package com.baeldung.modules.hello;

public class HelloModules {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }
}

И, наконец, в корневом каталоге hello.modules добавьте дескриптор модуля; module-info.java :

module hello.modules {
    exports com.baeldung.modules.hello;
}

Чтобы этот пример был простым, все, что мы делаем, – это экспортируем все открытые члены com.baeldung.modules.привет пакет.

7.3. Наш Второй Модуль

Наш первый модуль великолепен, но он ничего не делает.

Мы можем создать второй модуль, который использует его сейчас.

В нашем каталоге simple-modules создайте другой каталог модулей с именем main.app . На этот раз мы начнем с дескриптора модуля:

module main.app {
    requires hello.modules;
}

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

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

Создайте новую структуру пакета: com.baeldung.modules.главная .

Теперь создайте новый файл класса с именем MainApp.java.

package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloModules;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();
    }
}

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

7.4. Создание Наших Модулей

Чтобы построить наш проект, мы можем создать простой скрипт bash и поместить его в корень нашего проекта.

Создайте файл с именем compile-simple-modules.sh :

#!/usr/bin/env bash
javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

Эта команда состоит из двух частей: команд javac и find .

Команда find просто выводит список всех . java файлы в нашем каталоге простых модулей. Затем мы можем передать этот список непосредственно в компилятор Java.

Единственное, что нам нужно сделать иначе, чем в старых версиях Java,-это предоставить параметр module-source-path , чтобы сообщить компилятору, что он создает модули.

Как только мы запустим эту команду, у нас будет папка outDir с двумя скомпилированными модулями внутри.

7.5. Запуск Нашего Кода

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

Создайте еще один файл в корне проекта: run-simple-module-app.sh .

#!/usr/bin/env bash
java --module-path outDir -m main.app/com.baeldung.modules.main.MainApp

Чтобы запустить модуль, мы должны предоставить по крайней мере путь к модулю и основной класс. Если все работает, вы должны увидеть:

>$ ./run-simple-module-app.sh 
Hello, Modules!

7.6. Добавление услуги

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

Мы собираемся посмотреть, как использовать предоставляет…с и использует директивы.

Начните с определения нового файла в hello.modules модуле с именем HelloInterface .java :

public interface HelloInterface {
    void sayHello();
}

Чтобы упростить задачу, мы собираемся реализовать этот интерфейс с помощью наших существующих HelloModules.java класс:

public class HelloModules implements HelloInterface {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }

    public void sayHello() {
        System.out.println("Hello!");
    }
}

Это все, что нам нужно сделать, чтобы создать сервис .

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

Добавьте следующее в свой module-info.java :

provides com.baeldung.modules.hello.HelloInterface with com.baeldung.modules.hello.HelloModules;

Как мы видим, мы объявляем интерфейс и какой класс его реализует.

Далее нам нужно использовать этот сервис . В нашем модуле main.app давайте добавим следующее в ваш module-info.java :

uses com.baeldung.modules.hello.HelloInterface;

Наконец, в нашем основном методе мы можем использовать эту услугу через ServiceLoader :

Iterable services = ServiceLoader.load(HelloInterface.class);
HelloInterface service = services.iterator().next();
service.sayHello();

Компиляция и запуск:

#> ./run-simple-module-app.sh 
Hello, Modules!
Hello!

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

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

Это делает наш код намного более безопасным с очень небольшими дополнительными накладными расходами.

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

8. Добавление модулей в Безымянный модуль

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

Если класс не является членом именованного модуля, то он будет автоматически рассматриваться как часть этого безымянного модуля.

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

В общем случае опция добавления именованных модулей в набор корневых модулей по умолчанию – -add-modules (,)* где | | – это имя модуля.

Например, для обеспечения доступа ко всем модулям java.xml.bind синтаксис будет следующим:

--add-modules java.xml.bind

Чтобы использовать это в Maven, мы можем встроить то же самое в maven-compiler-plugin :


    org.apache.maven.plugins
    maven-compiler-plugin
    3.8.0
    
        9
        9
        
            --add-modules
            java.xml.bind
        
    

9. Заключение

В этом обширном руководстве мы сосредоточились на основах новой модульной системы Java 9 и рассмотрели их.

Мы начали с разговора о том, что такое модуль.

Далее мы говорили о том, как узнать, какие модули включены в JDK.

Мы также подробно рассмотрели файл декларации модуля.

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

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

Чтобы увидеть этот код и многое другое, обязательно проверьте его на Github .