воскресенье, 17 апреля 2016 г.

Jar Helsing - боремся с jar hell в springframework

О чем здесь?

Это статья о моей библиотеке Spring-Jar-Helsing, которую я создал в качестве легковесного средства, которое иногда полезно применить для того, чтобы побороть проблему Jar Hell в приложениях на базе Spring Framework

Суть проблемы

Современное программное обеспечение никогда не создаётся полностью с нуля. С целью экономии времени и денег на разработку, программы строятся из уже готовых библиотек. В наши дни, когда опенсурс движение набрало огромные обороты, и практически в каждой области можно найти готовую бесплатную библиотеку, следовать такому подходу проще простого, заиспользовать готовое и бесплатное ничего не стоит. Вопрос с разбуханием дистрибудива от большого количесва зависимостей, для серверных приложений в отличии от мобильных тоже не стоит, на сервере у нас всегда много места на диске. Таким образом даже небольшой микросервис на java состоит из десятков библиотек, а счёт библиотек в кровавомтиповом ынтерпрайз приложении идёт на сотни.

Однако медаль имеет и обратную сторону. Не только разработчики конечных продуктов любят переиспользовать готовые решения, часто разработчики библиотек тоже так делают, в итоге мы получаем библиотеки, которые зависят от других библиотек, а это уже при определенных условиях может серьезно усложнить нам жизнь. Например, если наше приложение зависит от библиотек A и B,  а те в свою очередь зависят от библиотеки C, но при этом A требует свою версию C, а B свою, иначе либо A либо B не работает, то мы можем столкнуться с проблемой которую в Java принято называть Jar hell. Собственно проблема относится не только к языку Java, по своей сути она глобальна и имеет обобщенное название Dependency Hell.

Попытаться решить проблему Dependency Hell нужно следующими способами:

  1. Попытаться найти такие версии A и B подхядище вашему приложению, которые зависили бы в идеальном случае от одной версии С, либо от версии C которая бинарно совместима с A и B.  В java как на уровне платформы, так и на уровне библитек лишний раз обратную совместимость стараются не ломать, так что вооружившись гуглом и терпением, можно пожанглировать с версиями и прийти к успеху.
  2. Если  общего знаменателя найти не удается, то можно пропатчить либо A, либо B, либо C и устранить несовместимость своими собственными руками. Этот путь может Вам иногда не подойти, либо по соображением трудоемкости создания форка, либо банально по соображениям лицензионной политики, иногда хотелось бы чонить форкнуть, да проприетарная лицензия не позволяет. 
  3. И так мы дошли до пункта 3 и мы в жопе - продукт выпускать надо, а значит нужно какое-то средсво которое позволило бы в одном приложении иметь сразу две версии библиотеки C. Тут для Вас как Spring разработчика на сцену выходит либо тяжелая артилерия в виде OSGI, обещающего решить большинство проблем с classpath, либо самодельные хаки с класслоадерами, которые в определенных юзкейсах внедрить намного быстрее и проще чем OSGI, и это не потребует зависимости от тяжлого OSGI контейнера. Spring-Jar-Helsing по сути является легковесной альтернативой OSGI, которую можно применять в ограниченных случаях, которые описанны далее.

Жизненные примеры использования Spring Jar Helsing

Использовать Jar Helsing в своей практике мне приходилось в двух случаях. Первый раз когда, он ещё не представлял из себя отдельную библиотеку. Работал я на госзаказе, попиливали мы баблишко создавая интеграционные приложения под Mule ESB которая плоть от крови базируется на springframework. Поскольку дело было в госсекторе, то приходилось иметь дело с особо извращенными крипто-алгоритмами естественно уже дадеными свыше в виде готовых библиотек обязательных к использованию под страхом смертной казни, и получилось так что Mule использовал библиотеку apache-codec(зачем-то форкнутую под нужды Mule), и библиотека для работы с цифровыми подписями, которую согласно ТЗ мы должны были использовать тоже внутри себя шла со своим форком apache-codec, и получалсь так, что эти форки apache-codec оказались несовместимыми и не работала либо базовая функциональность Mule, либо библиотека для создания криптоподписей. Не Mule, не криптобиблиотеку из-за политических ограничений патчить не было никакой возможности, поэтому проблему я решил грязным хаком. Вынес в отдельный артефакт в который выделил API для работы с цифровыми подписями, а реализацию по интеграции с криптобиблиотекой вынес в отдельный модуль, так чтобы основная часть приложения зависела от API но не зависела от реализации. Джарник реализации подгружал собственно ручно написанным класслоадером который работал таким образом чтобы при резолвинге классов для криптобиблиотеки классы для apache-codec брались из специального места которое не входило в основной classpath приложения. Когда нужно было поставть куданить подпись, приложение делало это через API даже не подозревая, что реализация работает по сути на отдельном classpath.

Когда столкнулся с аналогичной проблемой втрой раз, я решил оформить свои костыли с класслоадерами в повторно используемую библиотеку. Случилось это при использовании системы нагрузочного тестирования Jagger, которая тоже вся на спринге, и нагрузочные сценарии к ней пишутся в виде обычных спринговых бинов. Jagger очень многое делает для разработчика, и равномерно распределяет нагрузку по кластеру нагрузчиков, и собирает релультаты с кластера нагрузчиков, агрегирует их и сохраяет в базу, и отчеты генерит, и вебморда есть для просмотра результатов сессий, короче классная штука, за исключением того что у него самого около сотни библиотек в зависимостях, а такую важную вещь как загрузку пользовательских сценариев в отдельном класслоадере разработчики не предусмотрели. В итоге случилось так, что Jagger использует guava древней версии, а тестить мне нужно было, сервак который предоставляет thrift интерфейс и клиентское SDK для работы через thrift, и этот SDK тоже зависил от guava но самой свежей. Эти редискиразработчики guava ломают обратную совместимость в guava как нефиг делать. Поскольку разбег между версиями guava составлял 6 мажорных версий, то не удивительно что я столкнулся с множественными несовместимостями, когда методы удалены, переименованны или перемещенны, классы перемещены или удаленны. Тут конечно я имел пространство для маневра, я мог и патчить Jagger, и отказаться от использования SDK перейдя на работу напрямую с thrift, можно было и написать свою обертку без Guava, можно было собрать свою guava совместимую и с Jagger и с SDK, но я счел это нерациональным, в виду того что повторение хака с класслоадерами у меня заняло меньше времени. Точно тем же способом я загрузил бины своих нагрузчиков отдельным класслоадером и закрыл проблему.

Ключевые информация про Spring Jar Helsing

Я называю Spring-Jar-Helsing легковесным, потому что он не такой монструозный как OSGI состоит из всего двух классов, ресурсов никаких не жрёт, и внедрить его в приложение можно достаточно быстро и не надо читать килотонны документации и натаптывать киллометры конфигурационных файлов. Для того, чтобы начать работать достаточно прочитать README проекта на github и посмотреть пример использования там же на github.

Разберем все классы по косточкам:
  • JarHelsingClassLoader - утилитарный класс отвечающий за загрузку классов из кастомных путей не входящих в основной classpath приложения. Его реализация работает немного не так как написанно в рекомендациях из джавадоков по реализации класслоадеров, а именно при необходимости загрузить класс, он сначала смотрит в свой кастомный список ресурсов, и лишь в случае если класс не найден он делегирует загрузку родительском класслоадеру, но именно это и нужно, иначе мы бы не получили возможность иметь одну версию библиотеки классы коорой загруженны родительским класслоадером, и другую версию классы которой загруженны нашим класслоадером. JarHelsingClassLoader очень тонкий класс, всю тяжелую работу он делегирует классу URLClassLoader из JDK от которого он наследуется. Наследование от URLClassLoader позволяет из коробки получить множество способов задать кастомный класспаз, через файловые пути, ссылки на сетевые ресурсы, jar архивы внутри других jar архивов, короче всё на что может быть описанно как URL можно смело использовать при составлении кастомного classpath. Возможность ссылаться на Jar внутри Jar это очень удобная штука, классы находящиеся в Jar внутри другого Jar никогда не попадают в основной classpath, а вот сослаться на такой jar очень просто, так как java поддерживает такие URL на ресурсы внутри JAR из коробки, а засунуть при сборки приложения один JAR в другой не должно составлять больших трудностей для благородного дона.
  • JarHelsingBeanFactoryPostProcessor - это главная часть. Будучи объявленной в каком-нибудь спринговом контексте, он создает новый контекст, бины которого могут иметь свой отдельный classpath, а затем экспортирует все синглтоны из созданного контекста в главный с сохранением имён, из главного конекста можно ссылаться потом на такие бины по имени. При объявлении нужно задать два апарметра список XML файлов в которых сконфигурированны бины вспомогательного контекста и пути составляющие кастомный classpath. Объявление корневой точки вспомогательного контекста поддерживается из коробки только для XML, ежели он вам постыл то можно внутри XML настроить уже как надо DI на аннотациях и прочие упростяшки.  Пример объявления JarHelsingBeanFactoryPostProcessor
<bean class="com.github.springjarhelsing.JarHelsingBeanFactoryPostProcessor">
        <property name="resourceLocations">
            <list>
                <value>classpath:context-for-beans-with-custom-classpath.xml</value>
            </list>
        </property>
        <property name="overridenClasspathUrls">
            <list>
                <value>file:/opt/mycompany/libs/something-lib.jar</value> <!-- Points to file -->
                <value>http:mycomapny.com/java-libs/yet-another-library.jar</value> <!-- Points to file in internet -->
                <value>classpath:custom-libs/killer-library-6.6.6.jar</value> <!-- Points to file accessible as resource. Pay double attention that this file should not be by itself a valid source for parent classloader which can be used by parent classloader to class resolution -->
            </list>
        </property>
    </bean>

Если что то еще остается не ясным как начать использовать Spring Jar Helsing, то обратитесь к примеру, в рамках которого продемонстированно как внутри одного spring приложения использовать две разных версии guava.

Область эффективного применения и ограничения

  • Первый случай, когда  очевидно Spring-Jar-Helsing вам не нужен как средсво борьбы с Jar Hell, это когда ваше приложение уже работает на чем-то тяжеловесном вроде OSGI или полноценном JEE сервере с поддержкой EAR архивов. Поскольку там проблемы поиметь отдельный classpath для чего-то решены на высшем уровне, то тащить Jar Helsing нет смысла, просто правильно разбейте приложение на модули.
  • Так же, увы Jar Helsing ничем не поможет, если у Вас слоны отказываются стоять на черепахах, например вам нужны две разных версии системообразующих библиотек например таких как spring или hibernate. В общем Jar Helsing хорош когда можно четко очертить границы того, что требует специфический classpath, закрыть это интерфейсами не содержащими детали реализации, а реализацию выпинать в отдельный модуль и в Runtime прогружать с отдельным classpath. То есть Jar Helsing это скальпель для хирургических микроопераций.