Java всегда была одним из самых популярных языков программирования, применяемых в самых разных сферах — от серверных приложений до мобильных и встроенных систем. Однако с ростом масштабов и сложности проектов становился очевиден один существенный недостаток — почти полное отсутствие продвинутых средств для настоящей модульности. В попытке решить эту проблему многие разработчики обратили внимание на фреймворк OSGi, который предлагал зрелую архитектуру для построения модульных Java-приложений. Тем не менее, опыт внедрения OSGi далеко не всегда оправдывал ожидания, приводя порой к серьезным техническим и организационным сложностям, о чём свидетельствует история внедрения этого решения в одном из проектов — JQM, собственном open-source менеджере очередей заданий. Основная идея модульности — это чёткое разделение границ между независимыми частями приложения, которые могут быть разработаны, обновлены и заменены отдельно друг от друга.
Для JQM одним из ключевых требований была возможность легко добавлять заказные расширения в заранее определённые точки расширения системы. Такое разделение должно было упростить поддержку и расширение функционала без риска нарушить целостность основного приложения. На первый взгляд, OSGi представлялся почти идеальным решением — сам фреймворк изначально ориентирован на модульность, а в JQM код уже изначально был аккуратно разложен по ясным точкам расширения. Но реальность оказалась гораздо сложнее. Главным вызовом стала изоляция модулей, которую OSGi строго соблюдает.
В теории, каждый модуль должен видеть только интерфейсы и выделенные контракты, не имея прямого доступа к внутренностям других модулей. Подобный подход отлично работает при чистой архитектуре и корректном использовании принципов инкапсуляции. Однако в старом коде JQM через годы разработки накопилось огромное количество упрощений и обходных путей, например, прямых обращений к внутренним полям и методам между компонентами. Такое тесное переплетение кода нарушает идею модульности и превращает задачу разбиения системы на настоящие модули в сложнейший ребус. Зачастую единственным решением было либо чистка и переписывание частей кода, либо сворачивание в один общий модуль, что противоречит самой сути модульности.
Другим значительным препятствием стала поддержка жизненного цикла модулей. JQM, как специализированный сервер приложений с асинхронными заданиями, потребовал сложного процесса запуска, включающего чтение и инициализацию метаданных, загрузку всех плагинов и обеспечение их корректного взаимодействия в рантайме. Подключение внешнего механизма управления жизненным циклом, которым и является OSGi, оказалось нетривиальной задачей. Появилась необходимость тщательно координировать загрузку и инициализацию компонентов, чтобы избежать ошибок и сбоев. Отдельная «головная боль» появилась с внешними библиотеками.
Не все популярные Java-библиотеки содержат манифесты или метаданные, необходимые для корректной работы в среде OSGi. Зачастую приходилось вручную модифицировать и переконфигурировать сборочные процессы и зависимости, что наталкивалось на дополнительный дискомфорт, особенно когда приходилось осваивать сложный и, по мнению многих, неудобный инструмент Gradle. Хотя проект PAX предоставил средства для динамической инкапсуляции, они не стали панацеей, а лишь частично облегчили проблему. Но ещё более серьёзной оказалась проблема с механизмом загрузки классов. Комплексные Java-фреймворки, такие как JPA, JAX-RS или JAXB, используют разнообразные хитрые техники загрузки классов и позднего связывания, чтобы обеспечить гибкость и расширяемость.
Они опираются на Thread Context Class Loader, SPI, ServiceLoader и дополнительные «магические» методы. В этой среде строгие ограничения по видимости, вводимые OSGi, приводят к конфликтам и непредсказуемым ошибкам. Попытки обхода таких проблем иногда приводят к ещё большей путанице, например, через проекты вроде OSGi ServiceLoader Mediator (SPI Fly). Он динамически меняет контекст загрузчика с помощью инъекции байт-кода, чтобы обеспечить работу стандартных механик SPI. Но документация по этому инструменту крайне бедна, а ошибки очень трудно диагностируются, из-за чего разработчикам зачастую приходилось перекладывать ответственность на ручное конфигурирование и отказ от «магии» автодискавери.
Принципиальным отличием OSGi от более современного решения JPMS является подход к зависимости и сборке. Для OSGi характерно разделение путей сборки и исполнения, что порождает дополнительные сложности в управлении зависимостями и их версиями. В отличие от него, JPMS строится вокруг единой концепции модулей без отдельного механизма упаковки. В проекте JQM возникло желание сохранить Maven как единую и центральную систему сборки и управления зависимостями, что было сложно совмещать с упаковочными особенностями и зависимостями OSGi. Пришлось значительно модифицировать конфигурацию Maven-плагина, используя многочисленные исключения и хаков для исключения некоторых транзитивных зависимостей, что усложнило сопровождение.
Документация и поддержка — ещё одна серьёзная проблема. Основные материалы по OSGi часто написаны в виде спецификаций для реализации, без достаточного объёма практических рекомендаций для разработчиков. Большинство популярных реализаций — Apache Felix и Eclipse Equinox — имеют очень скудные и порой устаревшие заметки, а изучение вопроса сильно затрудняется из-за небольшого и ограниченного сообщества. Попытки создать стартовые шаблоны через проекты, как OSGi En-Route, заканчиваются наблюдениями о слабой стабильности примеров и устаревании. При возникновении нетривиальных проблем с, например, модульным HTTP-сервисом, приходится искать находки в трудно обнаруживаемых баг-трекерах или переписывать части логики вручную.
Также обременительны ошибки и исключения, которые сложно трактовать без глубокого знания внутренностей платформы. Часто причины сбоев оказываются завуалированными и требуют длительного поиска, а учёба подобных тонкостей сильно тратит силы и время. Примером служит NullReferenceException внутри SPI Fly, который на самом деле означает отсутствие корректного манифеста в Jar-файле. Что касается тестирования, ситуация выглядит не проще. Использование JUnit-тестов совместно с OSGi требует специальных инструментов и тонкой настройки.
PAX-Exam стал практически единственным способом адаптации существующих тестов, запуская их внутри полноценного OSGi-контейнера. При этом приходится бороться с проблемами различий в загрузчиках классов и сложности логирования, вызванных особенностями ограничения классового пространства OSGi. И нельзя не упомянуть практические проблемы, возникающие при использовании популярных библиотек. Случайно сломавшаяся функциональность JAXB после небольшого обновления библиотек, сложнейшее понимание механики частичных бандлов и динамических импортах, и жёсткие требования к набору зависимостей для работы JAX-RS whiteboard — все это только прибавляет разочарования и увеличивает порог вхождения. В итоге, несмотря на все сложности, команда не сожалеет о проделанной работе.
Вынужденная рефакторизация открыла глаза на слабые места архитектуры и улучшила общую читаемость кода. Но при этом получилось ясно и однозначно — сама модель и реализация OSGi, несмотря на гениальные задумки и полезные инструменты, остаются сложным «хаками» над платформой Java, которые с годами лишь наращивают технический долг и сложности поддержки. После смерти OSGi Foundation стало понятно, что эра OSGi подходит к концу, а будущее Java – за JPMS, официальным модульным решением от Oracle и сообщества. Команда JQM уже сделала решительный шаг на этот путь, удалив все следы OSGi из проекта и успешно внедрив JPMS. Опыт внедрения OSGi в JQM — это живой урок о том, насколько сложно заставить Java работать полностью модульно в реальных условиях, с legacy-кодом и внешними маркет-посредниками.