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'

и начните использовать в ваших проектах!
Поделиться с друзьями
-->

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


  1. sshikov
    20.12.2016 11:50

    А в чем смысл поддержки протоколов camel?

    Помнится если в camel написать URI вида «xxx: что-то-там», то в реестре, например Spring или OSGI, ищется xxx — который может быть настроенным компонентом (и во многих случаях является таковым). Т.е. скажем, если вы просто создадите компонент camel-activemq, то настройкам брокера будет взяться негде.

    Ну т.е. тут просто загрузки класса и создания компонента в общем-то не всегда достаточно. Как это устроено?


    1. igor_suhorukov
      20.12.2016 12:02
      +1

      Смысл в том, чтобы в унаследованном приложении без модификации его кода просто добавить еще одну библиотеку в classpath и читать данные, например, из hadoop file system (HDFS) или считать файл по scp. Просто за счет правки конфигурации приложения. Можно читать почти все что можно сконфигурировать в строке URI, насчет реестра вы правы — такой функциональности нет!

      Ну и как экзотика — просто получить кадр с вебкамеры

      new  java.net.URL("camel:/webcam:spycam?resolution=HD720").openStream()
      


  1. Throwable
    20.12.2016 12:27

    Очень интересно! Как я понял, MavenClassLoader по дефолту автоматически загружает все зависимости в один модуль. Если я хочу изолировать некоторые зависимости в отдельные модули, я их загружаю отдельно, а затем использую вариант с parent-ом — зависимости заново загружаться не будут?


    1. igor_suhorukov
      20.12.2016 12:36
      +1

      Да, верно надо колдовать с parent загрузчиком и в таких случаях поможет вариант с exclude в API ClassLoaderBuilder:

      forMavenCoordinates(MavenDependency[] dependencies, ClassLoader parent)
      

      а при создании MavenDependency можно указать какие транзитивные зависимости не надо разрешать конкретно в этом загрузчике классов
      new MavenDependency(String groupArtifactVersion, Collection<String> excludes)
      


  1. vadimhomchik
    21.12.2016 13:14

    Интересное решение. Сейчас используем jboss-modules, как альтернативу OSGI. Но несколько напрягает отсутствие нормальной документации.


    Хотел уточнить, в mvn-classloader есть ли возможность постоянно проверять и скачивать последний снапшот зависимостей? (попробовал использовать RemoteRepository и RepositoryPolicy, но что-то не вышло). И можно ли указать директорию для загружаемых артефактов?


    1. igor_suhorukov
      21.12.2016 13:27
      +1

      При старте JVM нужно указать параметр -DmavenSettings=путь к вашему settings.xml
      и указать в нем директорию куда сохранять артефакты

      <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                              http://maven.apache.org/xsd/settings-1.0.0.xsd">
      
      <localRepository>myrepo</localRepository>
      
      </settings>
      


      Со снепшотами я не работал — не было нужно. Попробую на следующей неделе. Для релизных версий отлично работает LATEST вместо version.


      1. vadimhomchik
        21.12.2016 14:27

        Спасибо!


    1. igor_suhorukov
      21.12.2016 13:33
      +1

      Попробуйте решение с диапазоном версий. Как в примере с (0,]

      Пришлось экранировать ] с помощью кавычек в bash
      igor@igor-comp:~/dev$ java -DmavenSettings=settings.xml -Dos.detected.classifier=windows-x86_64 -jar /home/igor/.m2/repository/com/github/igor-suhorukov/mvn-classloader/1.8/mvn-classloader-1.8.jar «org.littleshoot:littleproxy:(0,]» org.littleshoot.proxy.Launcher

      [Dropship WARN] No dropship.properties found! Using .dropship-prefixed system properties (-D)
      [Dropship INFO] Starting Dropship v0.0
      [Dropship INFO] Requested org.littleshoot:littleproxy:(0,], will load artifact and dependencies for org.littleshoot:littleproxy:(0,].
      [Dropship INFO] Collecting maven metadata.
      [Dropship INFO] Resolving dependencies.
      [Dropship INFO] Downloaded org.littleshoot:littleproxy:1.1.0 (24426 bytes) in 122,000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded org.sonatype.oss:oss-parent:7 (4824 bytes) in 91,0000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded com.google.guava:guava:18.0 (5666 bytes) in 88,0000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded com.google.guava:guava-parent:18.0 (7686 bytes) in 86,0000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded commons-cli:commons-cli:1.3.1 (10393 bytes) in 89,0000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded org.apache.commons:commons-parent:37 (63042 bytes) in 147,000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded org.apache:apache:16 (15397 bytes) in 118,000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded commons-codec:commons-codec:1.10 (11609 bytes) in 90,0000ms (Infinity kbytes/sec).
      [Dropship INFO] Downloaded org.apache.commons:commons-parent:35 (57772 bytes) in 156,000ms (Infinity kbytes/sec).


      1. vadimhomchik
        21.12.2016 14:27
        +1

        Попробовал сделать так:


        RepositoryPolicy policy = new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_ALWAYS, 
        RepositoryPolicy.CHECKSUM_POLICY_WARN);
        
        RemoteRepository repo = new RemoteRepository.Builder("custom", "default", repoUrl)
                        .setReleasePolicy(policy).setSnapshotPolicy(policy).build();

        Так вот при последующем вызове


        MavenClassLoader.usingRemoteRepositories(repo).forMavenCoordinates(...)

        с суфиксом -SNAPSHOT все работает (т.е. постоянно проверяет и обновляет), а без (релиз полиси) — нет. Странно, ведь полиси одинакова. LATEST и (0,] так же не помогли.


        Но в принципе этого уже достаточно.


        1. igor_suhorukov
          21.12.2016 14:41
          +1

          Спасибо, Вадим! Интересные вещи «раскопали», надо будет почитать документацию и подебажить как доберусь до IDE)


          1. vadimhomchik
            21.12.2016 14:59
            +1

            Супер! Дайте знать тогда, что в итоге получится! :) Спасибо!


      1. vadimhomchik
        21.12.2016 14:33

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


  1. relgames
    21.12.2016 16:52

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

    Почему? Разве нельзя cast сделать?


    1. Regis
      21.12.2016 18:03

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


      1. relgames
        22.12.2016 13:45

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


        1. igor_suhorukov
          26.12.2016 10:58

          Ваше право) Я как раз много где использую библиотеку из-за ее плюсов. В отличии от OSHI бандлов ничего не надо делать с исходной библиотекой и при этом можно изолировать разные версии одной и той же библиотеки в транзитивных зависимостях.


          1. sshikov
            28.12.2016 15:44

            Ну, честно говоря, для OSGI мало что надо делать с исходной библиотекой. Очень мало.


            1. igor_suhorukov
              28.12.2016 17:02

              К сожалению, я столкнулся на работе с существующей системой с несколькими десятками модулей. И вот там с не OSGI зависимостями была беда-беда. maven-bundle-plugin не решает и десятой части всех проблем в том проекте


              1. sshikov
                30.12.2016 11:50

                Странно. Не приходилось с таким сталкиваться — у нас karaf, либо в чистом виде, либо в виде jboss fuse, и не OSGI просто деплоятся как wrap, и никаких проблем и ними нет. При сотнях модулей в сумме.


      1. igor_suhorukov
        26.12.2016 10:55

        Да, именно про такой случай использования рассказывал!


    1. igor_suhorukov
      26.12.2016 10:54

      Можно, но тогда API библиотеку надо добавлять в classpath приложения и для mvn загрузчика нужно указать как parent classloader загрузчик вашего класса. Есть более удобный вариант для того же самого и про него расскажу на хабре в ближайшее время.