Привет, Хабр!
Система модулей в 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.