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

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

1. обзор

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

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

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

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

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

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

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

2.1. пакеты

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

Когда мы создаем модуль,we organize the code internally in packages just like we previously did with any other project.

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

2.2. Ресурсы

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

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

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

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

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

  • Name - имя нашего модуля

  • Dependencies - список других модулей, от которых зависит этот модуль

  • Public Packages - список всех пакетов, которые мы хотим получить доступными извне модуля

  • Services Offered - мы можем предоставить сервисные реализации, которые могут использоваться другими модулями

  • Services Consumed - позволяет текущему модулю быть потребителем услуги

  • Reflection Permissions - явно позволяет другим классам использовать отражение для доступа к закрытым членам пакета

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

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

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

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

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

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

  • System Modules – Это модули, перечисленные при запуске командыlist-modules выше. Они включают в себя модули Java SE и JDK.

  • Application Modules - Эти модули мы обычно хотим построить, когда решаем использовать модули. Они названы и определены в скомпилированном файлеmodule-info.class, включенном в собранный JAR.

  • Automatic Modules - Мы можем включать неофициальные модули, добавляя существующие файлы JAR в путь к модулю. Название модуля будет производным от имени JAR. Автоматические модули будут иметь полный доступ для чтения ко всем другим модулям, загруженным по пути.

  • Unnamed Module - Когда класс или JAR загружаются в путь к классам, но не в путь модуля, он автоматически добавляется в безымянный модуль. Это универсальный модуль для обеспечения обратной совместимости с ранее написанным кодом Java.

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

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

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

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

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

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

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

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

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

java --list-modules

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

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

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

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

И наконец,anything that is Oracle-specific is in the oracle modules.

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

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

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

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

module myModuleName {
    // all directives are optional
}

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

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

Вот где приходят директивы модуля.

4.1. требует

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

module my.module {
    requires module.name;
}

Теперьmy.module имеетboth a runtime and a compile-time dependency наmodule.name.

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

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

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

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

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

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

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

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

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

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

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

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

4.4. экспорт

By default, a module doesn’t expose any of its API to other modules. Этотstrong encapsulation был одним из ключевых факторов, мотивирующих создание модульной системы.

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

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

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

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

4.5. Экспорт… в

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

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

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

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

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

4.6. Uses

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

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

Обратите внимание, чтоthe class name we use is either the interface or abstract class of the service, not the implementation class:

module my.module {
    uses class.name;
}

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

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

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

4.7. Предоставляет… с

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

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

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

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

module my.module {
    provides MyInterface with MyInterfaceImpl;
}

4.8. Open

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

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

Поскольку Java 9 применяетstrong encapsulation,we now have to explicitly grant permission for other modules to reflect on our classes.

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

open module my.module {
}

4.9. Открытие

Если нам нужно разрешить отражение частных типов, но мы не хотим, чтобы весь наш код был открыт,we can use the opens directive to expose specific packages.

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

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

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

Хорошо, иногда отражение - это здорово, но мы все же хотим максимальной безопасности, которую мы можем получить отencapsulation. We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive:

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

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

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

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

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

  • add-reads - Вместо того, чтобы полагаться на файл объявления модуля, мы можем использовать командную строку, эквивалентную директивеrequires; –add-reads.

  • add-exports– Замена командной строки для директивыexports.

  • add-opens–  Заменить предложениеopen в файле объявления модуля.

  • add-modules–  Добавляет список модулей в набор модулей по умолчанию

  • list-modules–  Распечатывает список всех модулей и их строки версий

  • patch-module - Добавить или переопределить классы в модулях

  • illegal-access=permit|warn|deny - либо ослабить строгую инкапсуляцию, показывая одно глобальное предупреждение, либо отображать все предупреждения, либо завершаться ошибкой. По умолчаниюpermit.

6. видимость

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

A lot of libraries depend on reflection to work their magic (на ум приходят JUnit и Spring).

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

Мы можем использовать параметрыopen,opens иopens…to, чтобы предоставить доступ только во время выполнения для отражения. Примечание,this is runtime-only!

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

Если у нас должен быть доступ к модулю для отражения, и мы не являемся владельцем этого модуля (т.е. мы не можем использовать директиву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
      |- example
        |- modules
          |- hello
  |- main.app
    |- com
      |- example
        |- modules
          |- main

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

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

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

We can name this anything we want but follow package naming rules (т.е. точки для разделения слов и т. д.). Мы можем даже использовать имя нашего основного пакета в качестве имени модуля, если мы хотим, но обычно мы хотим придерживаться того же имени, которое мы использовали бы для создания JAR этого модуля.

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

com.example.modules.hello

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

package com.example.modules.hello;

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

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

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

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

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

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

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

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

module main.app {
    requires hello.modules;
}

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

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

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

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

package com.example.modules.main;

import com.example.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.example.modules.main.MainApp

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

>$ ./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!");
    }
}

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

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

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

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

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

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

uses com.example.modules.hello.HelloInterface;

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

HelloModules module = new HelloModules();
module.sayHello();

Скомпилируйте и запустите:

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

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

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

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

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

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

The unnamed module concept is similar to the default package. Поэтому он не считается реальным модулем, но может рассматриваться как модуль по умолчанию.

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

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

В общем случаеthe option to add the named modules to the default set of root modules is *–add-modules <module>*(,<module>)*, где<module> - имя модуля.

Например, для обеспечения доступа ко всем модулям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 Module.

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

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

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

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

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

Чтобы увидеть этот и другие коды, не забудьтеcheck it out over on Github.