Pull to refresh

Модуляризация в JavaSE без OSGI и Jigsaw

Reading time 7 min
Views 6.6K
mvn-classloader — загрузчик классов и ресурсов из maven совместимых репозитариев. Этот проект позволяет добавить ограниченную по возможностям и не сложную систему модулей в JavaSE приложение, где не нужна вся мощь и сложность OSGI.


Про то что еще позволяет делать mvn-classloader кроме модулей узнаете в статье.

Идея написать про вполне очевидные вещи возникла, когда прочитал одну из популярных статей на dzone ModRun: Modularity for Java — Without Jigsaw и не смог удержаться прокомментировать ее. Грустно когда люди не умеют искать решения, создают очередной велосипед который «не дотягивает» по функционалу и не смотрят на то, что давно реализовано до них и есть в github и центральном репозитарии maven.

Для чего может быть полезен mvn-classloader


  • Загрузка классов из maven в изолированном загрузчике классов и сборка простой модульной системы на основе classloader;
  • MavenServiceLoader делает то что и java.util.ServiceLoader, но кроме того загружает сервисы из maven модулей;
  • Загрузка двоичных файлов и ресурсов из репозитариев;
  • Создание иерархии загрузчиков классов, если очень хочется запутаться в дереве загрузчиков и порядке инициализации.
  • Загрузка и запуск приложения из maven репозитария;
  • UniversalURLStreamHandlerFactory для поддержки сотни новых протоколов java.net.URL;
  • «Прокачка» механизма Grape из Groovy для работы с родным для maven провайдером Aether, а не кустарным Ivy.

Пару слов про работу в кровавом энтерпрайзе с изолированной сетью за проксирующими репозиториями где опять же maven стандарт де-факто. Этому проекту доступны настройки которые по-умолчанию читает maven из ~/.m2/settings.xml и ~/.m2/settings-security.xml. И если у вас правильно настроен maven и все работает, не потребуется какой-либо дополнительной конфигурации.
Переопределить же эти настройки можно с помощью system property (передаются как параметры с "-D" при старте виртуальной машины):

  • mavenSettings.offline=true — переводит Aether в режим offline и пытается найти модули только в локальном кеше maven репозитария.
  • mavenSettings — путь к файлу settings.xml
  • mavenSettingsSecurity — путь к файлу settings-security.xml

Проект доступен в центральном maven репозитарии com.github.igor-suhorukov:mvn-classloader:1.8 и на гитхабе.

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

Загрузка классов из maven


Точкой входа для создания загрузчика служит класс com.github.igorsuhorukov.smreed.dropship.MavenClassLoader.

Проще всего начать работу с методов forMavenCoordinates(...), которые загружают классы из центрального maven репозитария или его зеркала, указанного в секции mirror-of файла settings.xml.

Когда же хотите загрузить классы из другого репозитария по ссылке, вам нужно вызвать метод using(ссылка_на_ваш_maven_репозитарий).forMavenCoordinates(...).

Координатами артефакта в maven для загрузчика класса является формат groupId:artifactId[:extension[:classifier]]:version

Начнем с примера загрузки класса io.hawt.app.App из артефакта io.hawt:hawtio-app:2.0.0. Создадим изолированный загрузчик классов, где будут своя версия классов веб сервера jetty и логгера. Это нужно для запуска административной консоли в том же jvm процессе что и ваше приложение.

При работе с классами в java, прийдется использовать reflection:

Class<?> hawtIoConsole = MavenClassLoader.usingCentralRepo().forMavenCoordinates("io.hawt:hawtio-app:2.0.0").loadClass("io.hawt.app.App");
Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader());
Method main = hawtIoConsole.getMethod("main", String[].class);
main.invoke(null, (Object) new String[]{"--port","10090"});

В Groovy этот же код записывается лаконичнее:

def hawtIoConsole = MavenClassLoader.usingCentralRepo().forMavenCoordinates('io.hawt:hawtio-app:2.0.0').loadClass('io.hawt.app.App')
Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader())
hawtIoConsole.main('--port','10090')

Если же модулям приложения нужно взаимодействовать между собой, то не обойтись без нескольких своих загрузчиков, их иерархии и, возможно, вам поможет MavenServiceLoader как примитивная альтернатива сервисам OSGI. Краткий обзор использования нескольких classloader найдете в разделе «Создание иерархии загрузчиков классов».

MavenServiceLoader


Этот загрузчик сервисов загружает классы из maven модулей с помощью java.util.ServiceLoader. Но чтобы артефакт предоставлял сервис, нужно обязательно запаковать в META-INF/services артефакта дескриптор сервиса. Подробно про то как работает ServiceLoader можете прочитать в javadoc или подсмотреть как реализовано на примере сервиса в проектах camel-url-handler и vfs-url-handler.

В качестве примера, от того что вы передадите в переменной protocol — vfs или camel — загрузится реализация сервиса URLStreamHandlerFactory из соотвествующего артефакта vfs-url-handler или camel-url-handler.

String artifact = System.getProperty(protocol+"MvnUrlHandler", String.format("com.github.igor-suhorukov:%s-url-handler:LATEST",protocol));

Collection<URLStreamHandlerFactory> urlStreamHandlerFactories = MavenServiceLoader.loadServices(artifact, URLStreamHandlerFactory.class);

Загрузка двоичных файлов и ресурсов из репозитариев


С помощью проекта можно загружать любой двоичный артефакт из maven репозитария, просто указывая дополнительно в координатах артефакта classifier и type. Как пример приведу загрузку и запуск вебдрайвера для chrome. В webdriver для реального браузера кроме API на стороне клиента есть и исполнимая в отдельном процессе часть, которая делает браузер марионеткой приложения. Часто его скачивают вручную и указывают путь в приложении. Но зачем, если можно это сделать автоматически!?

В зависимости от того, какая операционная система, нужно передать в качестве значения os win32, linux64 или mac64. При этом win32 работает и на 64 разрядных системах:


String chromedriver = MavenClassLoader.usingCentralRepo().resolveArtifact("com.github.igor-suhorukov:chromedriver:bin:" + os + ":2.24").getFile();
// в случае linux нужно сделать в программе chmod(chromedriver);
System.setProperty("webdriver.chrome.driver", chromedriver);

Создание иерархии загрузчиков классов


Комбинируя создания загрузчиков классов из maven с помощью вызовов методов класса com.github.igorsuhorukov.smreed.dropship.MavenClassLoader

  • forMavenCoordinates(java.lang.String gav) — в простейшем случае изолированный загрузчик, который ничего не знает об уже загруженных приложением классах. Будет повторно загружать их же из maven зависимостей и позволяет достичь максимальной степени изоляции загрузки.
  • forMavenCoordinates(java.lang.String gav, java.lang.ClassLoader parent) — вызов этого метода позволяет указать родительский загрузчик классов. Изоляция модулей меньше, зато можно разделять уже загруженные классы, например логгеров, своего API и т.п. Часто parent загрузчик это такой же MavenClassLoader или, например, Thread.currentThread().getContextClassLoader()

И создания из множества этих загрузчиков нового агрегированного «корневого» загрузчика. Для этого можно использовать загрузчик классов из массива ClassLoader:

com.github.igorsuhorukov.codehaus.spice.classman.runtime.JoinClassLoader(ClassLoader[] classLoaders, ClassLoader parent)

Если перегнуть палку используя такой подход, через некоторое время ваше приложение по сложности загрузки классов превзойдет OSGI/JEE сервер. Ввязываться ли в это и путаться ли в иерархии загрузчиков классов — решать вам, как специалисту. Когда потребуется для решения задачи такой инструмент, он вам доступен. В сложных случаях и большом количестве зависимых модулей с сложным порядком инициализации OSGI будет лучшим решением, так же как и когда требуется динамическая перезагрузка классов.

Загрузка и запуск приложения из maven репозитария


mvn-classloader может загружать и запускать классы из maven артефактов. Это легко сделать — нужно лишь передать в командной строке параметры groupId:artifactId[:version] classname. Приведу примеры.

Запускаем http proxy одной командой:

java -Dos.detected.classifier=windows-x86_64 -jar mvn-classloader-1.8.jar org.littleshoot:littleproxy:1.1.0 org.littleshoot.proxy.Launcher

Или же запуск своего git сервера gitblit:

java -jar mvn-classloader-1.8.jar org.eclipse.jetty:jetty-runner:9.4.0.v20161208 org.eclipse.jetty.runner.Runner http://gitblit.github.io/gitblit-maven/com/gitblit/gitblit/1.8.0/gitblit-1.8.0.war

UniversalURLStreamHandlerFactory


UniversalURLStreamHandlerFactory — обработчик URL для подгружаемых реализаций протоколов и он доступен в mvn-classloader. Загружает их из maven репозитария либо использует локальный кеш репозитария.

Сейчас для URL поддерживаются протоколы:


Зарегистрировать универсальный обработчик в java программе просто — лишь добавить инициализацию перед использованием таких экзотических адресов в URL:


java.net.URL.setURLStreamHandlerFactory(new com.github.igorsuhorukov.url.handler.UniversalURLStreamHandlerFactory());

Но надо помнить об ограничении стандартной библиотеки java, что вызывать java.net.URL.setURLStreamHandlerFactory можно только один раз за все время работы программы.

Примеры его использования можете найти в статье java.net.URL или старый конь борозды не испортит

«Прокачка» механизма Grape из Groovy


mvn-classloader также содержит AetherGrapeEngine из проекта spring boot и минимум необходимых зависимостей для его работы.

Grape — механизм разрешения зависимостей, встроенный в язык Groovy но имеющий не лучшую реализацию на Ivy из коробки. Про это рассказывал в публикации Уличная магия в скриптах или что связывает Groovy, Ivy и Maven?. И почти в каждой моей статье в хабе groovy я привожу примеры. Самый последний пример про сигнализацию для холодильника с видеорегистрацией в виде одного groovy файла:

java -Dlogin=...YOUR_EMAIL...@mail.ru -Dpassword=******* -jar groovy-grape-aether-2.4.5.4.jar AlarmSystem.groovy

Заключение


Пару лет назад нашел, объединил и доработал несколько open source технологий в одном проекте. Все необходимые для работы зависимости запакованы в один файл mvn-classloader.jar. Как и у каждой технологии, у mvn-classloader есть своя область применимости — никакого динамизма и перезагрузки классов, как в OSGI в нем нет и работа с классами на более низком уровне. Но для перечисленных задач mvn-classloader может стать лучшим выбором, в том числе для проектов с микросервисной архитектурой без JEE. На основной работе активно использовал его для модификации и тестирования сложной распределенной системы в составе aspectj-scripting.

Если вам нужно что-либо из перечисленной в этой статье функциональности, то просто добавьте зависимость в сборку maven:

<dependency>
    <groupId>com.github.igor-suhorukov</groupId>
    <artifactId>mvn-classloader</artifactId>
    <version>1.8</version>
</dependency>

или gradle

compile group: 'com.github.igor-suhorukov', name: 'mvn-classloader', version: '1.8'

и начните использовать в ваших проектах!

Tags:
Hubs:
+7
Comments 22
Comments Comments 22

Articles