Перейти к основному содержимому

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

· 16 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

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

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

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

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

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

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

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

2.1. Пакеты

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

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

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

2.2. Ресурсы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

java --list-modules

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

Java -модули — это классы реализации базовой спецификации языка SE.

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

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

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

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

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

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

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

module myModuleName {
// all directives are optional
}

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

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

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

4.1. Требует

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

module my.module {
requires module.name;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

4.4. Экспорт

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

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

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

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

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

4.5. Экспорт … В

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

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

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

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

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

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

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

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

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

module my.module {
uses class.name;
}

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

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

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

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

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

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

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

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

module my.module {
provides MyInterface with MyInterfaceImpl;
}

4.8. Открытым

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

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

Поскольку в 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. Параметры командной строки

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

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

  • module-path мы используем параметр —module-path , чтобы указать путь к модулю. Это список из одного или нескольких каталогов, содержащих ваши модули.
  • add-reads — вместо того, чтобы полагаться на файл объявления модуля, мы можем использовать командную строку, эквивалентную директиве requires ; -добавить-читает .
  • add-exports замена командной строки для директивы экспорта .
  • add-opens заменить предложение 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
|- foreach
|- modules
|- hello
|- main.app
|- com
|- foreach
|- modules
|- main

7.2. Наш первый модуль

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

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

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

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

com.foreach.modules.hello

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

package com.foreach.modules.hello;

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

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

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

Чтобы не усложнять этот пример, все, что мы делаем, это экспортируем все общедоступные члены пакета com.foreach.modules.hello .

7.3. Наш второй модуль

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

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

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

module main.app {
requires hello.modules;
}

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

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

Создайте новую структуру пакета: com.foreach.modules.main .

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

package com.foreach.modules.main;

import com.foreach.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 файлы в нашем каталоге simple-modules. Затем мы можем передать этот список непосредственно компилятору Java.

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

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

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

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

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

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

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

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

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

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

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

Начните с определения нового файла в модуле 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.foreach.modules.hello.HelloInterface with com.foreach.modules.hello.HelloModules;

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

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

uses com.foreach.modules.hello.HelloInterface;

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

Iterable<HelloInterface> 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 <module> (,<module>)*, где <module> — это имя модуля.

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

--add-modules java.xml.bind

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

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>9</source>
<target>9</target>
<compilerArgs>
<arg>--add-modules</arg>
<arg>java.xml.bind</arg>
</compilerArgs>
</configuration>
</plugin>

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

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

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

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

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

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

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

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