Хочу поделиться решениями одной проблемы, с которой мне пришлось столкнуться, плюс исследование данного вопроса в контексте Java 9. Писатель из меня ещё тот (пишу в первый раз), поэтому закидывание вкусными помидорами с указанием причин только приветствуется.
Сразу договоримся, что статья не годится в качестве руководства по: Если по последним именам информации в сети полно, то по первому… со временем появится, по крайней мере здесь есть необходимая информация. Представим себе простую ситуацию: разворачиваем кластер Elasticsearch и загружаем в него данные. Мы пишем приложение, которое занимается поиском в этом кластере. Поскольку постоянно выходят новые версии Elasticsearch, мы привносим в кластер новые проблемы фичи с помощью rolling upgrade. Но вот незадача — в какой-то момент у нас сменился формат хранимых данных (например, чтобы максимально эффективно использовать какую-то из новых фич) и делать reindex нецелесообразно. Нам подойдёт такой вариант: ставим новый кластер на этих же машинах — первый кластер со старой схемой данных остаётся на месте только для поиска, а поступающие данные загружаем во второй с новой схемой. Тогда нашему поисковому компоненту потребуется держать на связи уже 2 кластера. Наше приложение использует Java API для общения с кластером, а это значит, что оно тянет в зависимостях сам Elasticsearch. Стоит отметить, что вместе с 5-ой версией вышел и Rest Client, избавляющий нас от таких проблем (а также от удобного API самого Elasticsearch), но мы переместимся во времени на момент релиза 2-ой версии. Рассмотрим возможные решения на примере простого приложения: поиск документа в 2-х кластерах Elasticsearch 1.7 и 2.4. Код доступен на гитхабе, и повторяет структуру данной статьи (отсутствует только OSGi).  Перейдём к делу. Создадим Maven-проект следующей структуры: Очевидно, что в одном модуле подключить несколько версий одной библиотеки не получится, поэтому проект должен быть многомодульным: Модуль core содержит класс Searcher, который является "испытателем" наших модулей es-v1 и es-v2: Ничего сверхестественного: выводится версия Elasticsearch, используемая модулем, и проводится тестовый поиск через него — этого будет достаточно для демонстрации. Взглянем на одну из реализаций, вторая почти идентична: Тоже всё просто: текущая версия, зашитая в Elasticsearch, и поиск по полю field во всех индексах (*), возвращающий первый найденный документ, если есть. Проблема здесь кроется в том, как именно вызвать реализации интерфейса SearchClient в методе Searcher#getClient и получить желаемый результат. Даже если вы не знаток Java, наверняка слышали, что там властвует ClassLoader. Он не позволит нам совершить задуманное, если оставить по умолчанию, поэтому такое решение влоб не сработает: Соберём, запустим и увидим результат… вполне неопределённый, например, такой: Хотя мог и ClassNotFoundException бросить… или ещё чего... Так как URLClassLoader найдёт и загрузит первый попавшийся класс с заданным именем из заданного набора jar-файлов и директорий, это будет необязательно требуемый класс. В данном случае эта ошибка возникает из-за того, что в списке class-path библиотека elasticsearch-2.4.5.jar идёт до elasticsearch-1.7.5.jar, поэтому все классы (которые совпадают по имени) будут загружены для 2.4.5. Поскольку наш Searcher сначала пытается загрузить модуль для Elasticsearch 1.7.5 (getClient("1")), URLClassLoader загрузит ему совсем не те классы... Когда загрузчик классов имеет в своём распоряжении пересекающиеся по имени (а значит и по именам файлов) классы, такое его состояние называют jar hell (или class-path hell). Становится очевидным, что модули и их зависимости нужно разнести по разным загрузчикам классов. Просто создаём URLClassLoader на каждый модуль es-v* и указываем каждому свою директорию с jar-файлами: Нам нужно собрать и скопировать все модули в соответствующие директории modules/es-v*/, для этого используем плагин maven-dependency-plugin в модулях es-v1 и es-v2.  Соберём проект: И запустим: Бинго! если не пропатчить JvmInfo, о чём упоминается ниже в пересборке Elasticsearch 1.7. Совсем хардкорный случай предполагает, что модуль core тоже использует какие-нибудь утилитные методы из библиотеки Elasticsearch. Наше текущее решение уже не сработает из-за порядка загрузки классов: То есть в этом случае будут загружены классы Elasticsearch из core, а не es-v*. Присмотревшись к порядку загрузки, видим обходной вариант: написать свой загрузчик классов, который нарушает этот порядок, поменяв местами шаги 2 и 3. Такой загрузчик сможет загрузить не только отдельно свой модуль es-v*, но и увидит классы из core. Напишем свой URLClassLoader, назовём его, например, ParentLastURLClassLoader: и переопределим loadClass(String,boolean), скопировав код из ClassLoader и убрав всё лишнее: Меняем местами вызовы getParent().loadClass(String) и findClass(String) и получаем: Поскольку наше приложение вручную будет загружать классы модуля, загрузчик должен бросить ClassNotFoundException, если класса нигде не найдено. Загрузчик написан, теперь используем его, заменив URLClassLoader в методе getClient(String): на ParentLastURLClassLoader: и запустив приложение снова, видим тот же результат: В Java 6 добавили класс java.util.ServiceLoader, который предоставляет механизм загрузки реализаций по интерфейсу/абстрактному классу. Этот класс тоже решает нашу проблему: Всё очень просто: Чтобы ServiceLoader увидел реализации интерфейсов, нужно создать файлы с полным именем интерфейса в директории META-INF/services: и написать полные имена классов, реализующих этот интерфейс, на каждой строке: в нашем случае по одному SearchClientImpl на каждый модуль.
Для es-v1 в файле будет строка: и для es-v2: Также используем плагин maven-dependency-plugin для копирования модулей es-v* в modules/es-v*/. Пересоберём проект: И запустим: Отлично, требуемый результат снова получен. Для упомянутого хардкорного случая придётся выносить интерфейс SearchClient в отдельный модуль spi и использовать следующую цепочку загрузчиков: т.е. Признаюсь сразу и честно — мне не доводилось сталкиваться с фреймворками OSGi (может оно и к лучшему?). Взглянув на маны для начинающих у Apache Felix и Eclipse Equinox, они больше походят на контейнеры, в которые (вручную?) загружают бандлы. Даже если есть реализации для встраивания, это слишком громоздко для нашего простого приложения. Если я не прав, выскажите обратную точку зрения в комментариях (да и вообще хочется увидеть, что его кто-то использует и как). Я не стал углубляться в этом вопросе, т.к. в Java 9 модули теперь из коробки, которые мы сейчас и рассмотрим. На прошлой неделе релизнулась 9-ая версия платформы, в которой главным нововведением стали модуляризация рантайма и исходников самой платформы. Как раз то, что нам надо! Hint: Для того, чтобы использовать модули, нужно сначала скачать и установить JDK 9, если вы ещё этого не делали. Здесь дело осложняет только способность используемых библиотек запускаться под девяткой в качестве модулей (на самом деле, я просто не нашёл способа в IntelliJ IDEA указать class-path вместе с module-path, поэтому далее мы всё делаем в контексте module-path). Прежде чем переходить к модификации кода нашего приложения под модульную систему, сначала узнаем, как она работает (я могу ошибаться, поскольку сам только начал разбираться в этом вопросе). Кроме упомянутых модулей, есть ещё слои, содержащие их. При старте приложения создаётся слой boot, в который загружаются все указанные в --module-path модули, из которых состоит приложение, и их зависимости (от модуля java.base автоматически зависят все модули). Другие слои могут быть созданы программно. Каждый слой имеет свой загрузчик классов (или иерархию). Как и у загрузчиков, модульные слои также могут быть построены иерархически. В такой иерархии модули одного слоя могут видеть другие модули, находящихся в родительском слое. Сами модули изолированы друг от друга и по умолчанию их пакеты и классы в них не видны другим модулям. Дескриптор модуля (им является module-info.java) позволяет указать, какие пакеты может открыть каждый модуль и от каких модулей и их пакетов зависят они сами. Дополнительно, модули могут объявлять о том, что они используют некоторые интерфейсы в своей работе, и могут объявлять о доступной реализации этих интерфейсов. Эта информация используется ServiceLoader API для загрузки реализаций интефрейсов из модулей. Модули бывают явными и автоматическими (типов больше, но мы ограничимся этими): Теперь этой информации будет достаточно, чтобы применить её на нашем проекте: Всё так просто?... Модулям не разрешено иметь пересекающиеся имена пакетов (по крайней мере в одном слое). Например, есть некая библиотека, которая предоставляет какое-то API в публичном классе Functions. В этой библиотеке есть класс Helpers с пакетной областью видимости. Вот они: На сцену выходит вторая библиотека, которая предоставляет которая дополняет функционал первой: И ей требуется некоторый функционал из закрытого класса Helpers. Выходим из положения, поместив какой-нибудь класс в этот же пакет: Поздравим себя — мы только что создали себе проблему разделения пакетов (split package) с точки зрения модульной системы. Что можно сделать с такими библиотеками? Нам предлагают оставить такие библиотеки в class-path и дотянуться до них из модулей, используя автоматические модули в качестве моста. Но мы не ищем лёгких путей, поэтому используем другой вариант: докладём в библиотеку все его зависимости и получим один единственный jar-архив (известный под названиями fat jar и uber jar), его-то и можно использовать в module-path как автоматический модуль, минуя class-path. Проблемой может стать сборка такого all-in-one jar. Elasticsearch активно использует доступ к package-private методам/полям для доступа к некоторому функционалу Lucene. Чтобы использовать его в виде автоматического модуля, сделаем из него uber jar и установим в локальный репозиторий под именем elasticsaerch-shaded для дальнейшего использования в нашем проекте. В первых версиях проект приложения представляет из себя единственный Maven-модуль, поэтому здесь проблем особо не возникнет: нужно поправить pom.xml и некоторые классы, если собираем 8-кой. Клонируем репозиторий в какую-нибудь директорию, чекаутим тег v1.7.5 и начинаем править: и желательно в оригинале, без перемещений: и оборнём её в "правильные": Эта информация используется всего лишь для статистики. Готово, теперь можно собрать jar: и после компиляции и сборки получим требуемый elasticsearch-1.7.5.jar в директории target. Теперь его нужно установить в локальный репозиторий, например, под именем elasticsearch-shaded: Теперь этот артефакт можно использовать как автоматический модуль в нашем Maven-модуле es-v1: Откатим локальные изменения и зачекаутим тег v2.4.5. Начиная со 2-ой версии проект разбит на модули. Нужный нам модуль, выдающий elasticsearch-2.4.5.jar — модуль core. Нам придётся добавить shade-плагин заново в модуль core, добавив в настройках трансформер сервис-файлов и исключение логгеров: и сменим импорты com.twitter.jsr166e на java.util.concurrent.atomic. Готово, теперь проделываем те же шаги по сборке и установке, что и для es-v1, с небольшими отличиями: После того, как мы выяснили, что используемые нами библиотеки способны работать в модульной системе Java, сделаем из наших Maven-модулей явные Java-модули. Для этого нам потребуется в каждой директории с исходниками (src/main/java) создать файлы module-info.java и в них описать взаимоотношения между модулями. Модуль core не зависит ни от каких других модулей, а только содержит интерфейс, который должны реализовать другие модули, поэтому описание будет выглядеть так: Для модулей es-v1 и es-v2 будет похожее описание: Итого имеем для es-v1: Для es-v2 почти всё тоже самое, только в именах должна фигурировать версия v2. Теперь как загрузить такие модули? Ответ на этот вопрос есть в описании класса ModuleLayer, который содержит небольшой пример загрузки модуля с ФС. Предположив, что модули es-v* находятся каждый всё в тех же директориях modules/es-v*/, можно написать примерно такую реализацию: ModuleLayer#defineModulesWithManyLoaders нам здесь не подойдёт, так как у нас получатся совсем изолированные модули и наши es-v* не смогут увидеть свои зависимости. Итак, теперь нужно собрать все модули. Для компиляции потребуется плагин maven-compiler-plugin поновее, на данный момент последняя версия — 3.7.0: Укажем Java 9 для исходников: И не забываем про maven-dependency-plugin, поэтому пересоберём модули: Теперь можно запустить, и видим такой же вывод: Может показаться, что решение с загрузчиком классов — проще, но решение из коробки просто глупо игнорировать, если только вы не собираетесь остаться на Java 8 или более ранней. Загрузка библиотеки разных версий выливается в механизм, очень напоминающий систему плагинов/модулей. Для этого можно: P.S. Желаю всем джавистам удачного перехода на Java 9! Пользователь Elasticsearch