Привет, Хабр!

Система модулей в Java 9, известная как Project Jigsaw, была задумана и реализована для решения ряда проблем, включая «Ад JAR‑файлов» и сложностей с обеспечением сильной инкапсуляции.

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

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

Рассмотрим, как выглядит работы с системой модулей в Java.

Структура и типы модулей

Системные модули составляют Java SE и JDE, предоставляя базовую функциональность, которая необходима каждому Java‑приложению. Используя команду java --list-modules, можно увидеть полный список доступных системных модулей, их очень много! Некоторые из них:

java.activation
java.base
java.compiler
java.corba
java.datatransfer
java.desktop
java.instrument
java.jnlp
java.logging
java.management
java.management.rmi
java.naming
java.prefs
java.rmi
java.scripting
java.se
java.se.ee
java.security.jgss
java.security.sasl
java.smartcardio
java.sql
java.sql.rowset

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

Автоматические модули, — это механизм, позволяющий использовать существующие JAR-файлы как модули, даже если они не были специально предназначены для работы в модульной системе. Достаточно поместить JAR-файл на --module-path, и Java автоматически создаст для него модуль, имя которого будет унаследовано от имени JAR-файла.

Безымянные модули, — это спасательный круг для всего кода, который не принадлежит ни одному из модулей на module-path. Все JAR-файлы, загруженные через --class-path, автоматически становятся частью безымянного модуля.

Если раньше всё вращалось вокруг class-path — универсального пути, по которому JVM искала все классы и библиотеки, то теперь module-path мастхев и юзается везде, предлагая более строгое и структурированное разделение зависимостей и модулей.

Определение модуля

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

Файл module-info.java - центр модулей в Java. Он определяет модуль, его зависимости, экспортируемые пакеты, используемые и предоставляемые услуги. Создание этого файла — первый шаг к созданию модуля:

module com.example.myModule {
    requires java.sql;
    exports com.example.myModule.api;
    uses com.example.myModule.spi.MyService;
    provides com.example.myModule.spi.MyService with com.example.myModule.internal.MyServiceImpl;
}

requires: указывает, что данный модуль зависит от другого модуля. Например, requires java.sql; говорит, что модуль нуждается в модуле java.sql для своей работы.

exports: делает пакеты доступными для других модулей, с помощью этого можно контролировать уровень доступа к компонентам приложения, улучшая инкапсуляцию. exports com.example.myModule.api; экспортирует пакет, делая его доступным для использования вне модуля.

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

uses com.example.myModule.spi.MyService;

provides ... with: объявляет, что модуль предоставляет реализацию для используемого сервиса. Это позволяет создать заменяемые компоненты в приложении. provides com.example.myModule.spi.MyService with com.example.myModule.internal.MyServiceImpl; указывает, что MyServiceImpl является реализацией MyService.

Рассмотрим другой пример создания модуля для библиотеки. В этом случае определяем модуль com.example.data, который требует ряд стандартных и сторонних модулей, экспортирует API для работы с данными и предоставляет реализацию через сервисы:

module com.example.data {
    requires java.logging;
    requires transitive com.example.utils;
    exports com.example.data.api;
    uses com.example.data.spi.DataService;
    provides com.example.data.spi.DataService with com.example.data.internal.DataServiceImpl;
}

Директива requires transitive com.example.utils; обеспечивает, что любой модуль, зависящий от com.example.data, автоматически получает доступ к com.example.utils.

Определение услуг

Услуга представляет собой интерфейс или абстрактный класс, а реализация услуги - это конкретный класс, который этот интерфейс реализует или от абстрактного класса наследуется. Для объявления услуги и её реализаций используются директивы uses и provides ... with в файле module-info.java.

Предположим, есть интерфейс ServiceInterface, который представляет услугу, и класс ServiceImpl, который представляет реализацию этой услуги:

package com.example.service;

public interface ServiceInterface {
    void execute();
}
package com.example.service.impl;

import com.example.service.ServiceInterface;

public class ServiceImpl implements ServiceInterface {
    @Override
    public void execute() {
        System.out.println("Service executed.");
    }
}

И теперь если у нас есть модуль com.example.serviceprovider, который предоставляет эту услугу, его module-info.java мог бы выглядеть так:

module com.example.serviceprovider {
    exports com.example.service;
    provides com.example.service.ServiceInterface with com.example.service.impl.ServiceImpl;
}

Модуль, который хочет использовать эту услугу, должен объявить это с помощью директивы uses в своем файле module-info.java.

Предположим, есть модуль com.example.serviceconsumer, который использует эту услугу:

module com.example.serviceconsumer {
    requires com.example.serviceprovider;
    uses com.example.service.ServiceInterface;
}

Во время выполнения, модуль может динамически обнаружить и использовать реализацию услуги с помощью ServiceLoader API. Например, модуль com.example.serviceconsumer может выполнить следующий код, чтобы использовать ServiceInterface:

import com.example.service.ServiceInterface;
import java.util.ServiceLoader;

public class ServiceConsumer {
    public static void main(String[] args) {
        ServiceLoader<ServiceInterface> serviceLoader = ServiceLoader.load(ServiceInterface.class);
        serviceLoader.forEach(ServiceInterface::execute);
    }
}

Пример: сервис конвертации валют

К примеру есть модуль com.example.currencyconverter, предоставляющий интерфейс CurrencyConverter для конвертации валют. Нам сказали, что нужно, чтобы реализации этого интерфейса могли предоставляться различными модулями, не меняя при этом код модуля, который использует сервис.

В модуле com.example.currencyconverter, мы определим интерфейс CurrencyConverter:

package com.example.currencyconverter.spi;

public interface CurrencyConverter {
    double convert(double amount, String fromCurrency, String toCurrency);
}

В module-info.java этого модуля объявим, что он использует сервис CurrencyConverter:

module com.example.currencyconverter {
    exports com.example.currencyconverter.spi;
    uses com.example.currencyconverter.spi.CurrencyConverter;
}

Теперь допустим, у нас есть другой модуль com.example.currencyprovider, который предоставляет реализацию этого интерфейса. В этом модуле мы определяем класс MyCurrencyConverter:

package com.example.currencyprovider;

import com.example.currencyconverter.spi.CurrencyConverter;

public class MyCurrencyConverter implements CurrencyConverter {
    public double convert(double amount, String fromCurrency, String toCurrency) {
        // Реализация конвертации валют
        return convertedValue;
    }
}

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

module com.example.currencyprovider {
    requires com.example.currencyconverter;
    provides com.example.currencyconverter.spi.CurrencyConverter with com.example.currencyprovider.MyCurrencyConverter;
}

Когда приложение запускается, модуль, использующий CurrencyConverter, может получить реализацию сервиса и использовать ее, не зная, какой именно класс ее предоставляет. Это делается с помощью ServiceLoader:

var serviceLoader = ServiceLoader.load(CurrencyConverter.class);
for (CurrencyConverter converter : serviceLoader) {
    double result = converter.convert(100, "USD", "EUR");
    System.out.println("Converted: " + result);
}

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

Сборка и запуск

Сначала создается файл module-info.java в корне каждого модуля приложения. Этот файл должен содержать информацию о модуле, включая его requires, exports, и услуги uses и provides.

Используем команду javac с указанием пути к модулям --module-path или -p и исходникам -d для указания директории назначения. Пример команды для модуля com.example.myapp:

javac -d mods/com.example.myapp --module-path libs --module-source-path src $(find src/com.example.myapp -name "*.java")

Это компилирует модуль в директорию mods/com.example.myapp.

После компиляции юзаем команду jar для создания модульного JAR файла. Указываем имя модуля с помощью параметра --module-version и путь к файлу module-info.class:

jar --create --file=libs/com.example.myapp@1.0.jar --module-version=1.0 -C mods/com.example.myapp/ .

Это создаст модульный JAR com.example.myapp@1.0.jar в папке libs.

При запуске приложения, использующего модульность, нужно указать путь к модулям с помощью параметра --module-path или -p.

Указываем главный модуль и класс с помощью параметра --module или -m, где com.example.myapp/com.example.myapp.Main указывает на главный класс Main в модуле com.example.myapp:

java --module-path libs -m com.example.myapp/com.example.myapp.Main

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

Java Platform Module System автоматически строит граф модулей, разрешая зависимости между модулями на основе информации, предоставленной в файлах module-info.java.

Все модули по умолчанию зависят от модуля java.base, который содержит основные классы Java, такие как java.lang и java.util. Стандартная библиотека Java доступна всем модулям без явного указания.


Статья подготовлена в преддверии запуска специализации Java Developer. В рамках запуска специализации пройдет бесплатный вебинар про многопоточность в Java.

Комментарии (3)


  1. aleksandy
    15.03.2024 17:22
    +1

    Определение услуг
    Услуга представляет

    Опосля гугло/яндекс/etc.-транслэйта текст кто-то вообще читал?

    Пример: сервис конвертации валют

    А чего вдруг? Почему не "услуга конвертации валют"?


  1. porn
    15.03.2024 17:22
    +1

    Вы только что систему модулей в Java.


  1. feech1
    15.03.2024 17:22

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

    Как это напоминает конфигурацию спринг контекста в XML.

    Новичок! Не создавай сервисы и реализации в своем "бизнес-коде" (как может показаться из этой статьи). Учи Понимай шаблоны программирования.