Статьи

Создание простого старого Java OSGi-совместимого

Хотя OSGi становится все более популярным в мире Java, существует много Java-приложений и библиотек, которые не предназначены для работы в OSGi. Иногда вам может потребоваться запустить такой код в среде OSGi, либо потому, что вы хотите воспользоваться преимуществами, предлагаемыми самой OSGi, либо потому, что вам нужны определенные функции, предлагаемые только этой конкретной средой. Часто вы не можете позволить себе полностью перейти на OSGi или, по крайней мере, вам нужен переходный период, в течение которого ваш код работает нормально как внутри, так и за пределами OSGi. И, конечно же, вы хотели бы сделать это с минимальными усилиями и без увеличения сложности вашего программного обеспечения.

Недавно наша команда в SAP столкнулась с аналогичной проблемой. У нас есть довольно большое устаревшее простое Java-приложение, которое с годами выросло и теперь включает в себя ряд собственных фреймворков и пользовательских решений. Нам нужно было предложить интерфейс на основе REST для этого приложения, поэтому нам пришлось либо включить веб-сервер, либо запустить его в среде, в которой он есть. Мы решили использовать SAP Lean Java Server (LJS) , движок, лежащий в основе SAP NetWeaver Cloud , который включает Tomcat и другие полезные сервисы. Однако LJS основан на Equinox , реализации OSGi от Eclipse, и поэтому нам нужно было убедиться, что наш код совместим с OSGi, чтобы обеспечить бесперебойную совместимость. В процессе мы узнали много нового по этой теме, и поэтому я хотел бы поделиться с вами нашими самыми интересными результатами в этом посте.

Для того чтобы простой Java-код работал гладко в среде OSGi, необходимо выполнить следующие предварительные условия:

  • Он упакован в пакеты OSGi , то есть архивы jar с действительными манифестами OSGi.
  • Он соответствует требованиям и ограничениям, установленным OSGi в отношении динамической загрузки классов.
  • Все его пакеты экспортируются только одним пакетом, то есть нет разных пакетов, экспортирующих один и тот же пакет.

Кроме того, во многих случаях вам может потребоваться создать новую точку входа для запуска приложения из консоли OSGi. Если вы используете Equinox, вам следует подумать о создании приложения Equinox для этой цели.

Обратите внимание, что выполнение вышеуказанных требований не означает, что вы никоим образом не должны переносить свой код в OSGi, чтобы он работал только в OSGi, и что вам не следует коренным образом изменять свою среду разработки или процесс на основе OSGi. Напротив, наш опыт показывает, что вполне возможно достичь совместимости с OSGi, не теряя возможности работать за пределами OSGi, и без существенного изменения подходов и инструментов разработки, выполняя указанные выше требования следующими способами:

  • Все манифесты OSGi могут генерироваться автоматически с использованием BND и других инструментов на его основе. За пределами OSGi эти манифесты не используются, но и не причиняют вреда.
  • Динамическая загрузка классов на основе Class.forName() и пользовательская загрузка классов могут быть заменены практически идентичными механизмами, которые используют собственные сервисы OSGi. Возможно динамическое переключение между исходным и OSGi-механизмами в зависимости от того, выполняется ли ваш код в OSGi с очень небольшими изменениями в существующем коде.
  • В качестве альтернативы вы можете полностью избавиться от динамической загрузки классов в OSGi, используя механизм служб OSGi для динамической регистрации и обнаружения «именованных» реализаций.
  • Идентичные пакеты, экспортируемые более чем одним пакетом, должны быть просто переименованы. Очевидно, что это работает и за пределами OSGi.
  • Зависимости от OSGi можно минимизировать, поместив весь специфичный для OSGi код в ограниченное количество пакетов, которые предпочтительно не содержат код, который также должен выполняться вне OSGi.

Следующие разделы предоставляют более подробную информацию о том, как этого можно достичь.

Упаковка как пакеты OSGi

Для работы в среде OSGi весь код Java должен быть упакован в пакеты OSGi. Это относится не только ко всем архивам, созданным вашей сборкой, но и ко всем их зависимостям, которые поставляются как часть вашего программного обеспечения.

Если ваша сборка использует Maven, вам следует рассмотреть возможность использования Maven Bundle Plugin (который внутренне использует BND) для создания допустимых манифестов OSGi для всех архивов, созданных при сборке. В большинстве случаев манифесты, генерируемые конфигурацией этого плагина по умолчанию, будут работать просто отлично. Однако в некоторых случаях для создания правильных манифестов могут потребоваться некоторые незначительные изменения и дополнения, например:

  • Добавление дополнительных пакетов импорта для классов, которые используются только через отражение и поэтому не могут быть найдены BND.
  • Указание XML-компонентов сервисных компонентов для пакетов, которые предоставляют декларативные сервисы OSGi.
  • Указание активаторов комплектов для комплектов, которые зависят от пользовательской активации.

В нашем проекте плагин комплекта настраивается в нашем родительском POM следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<properties>
    <classpath></classpath>
    <import-package>*</import-package>
    <export-package>{local-packages}</export-package>
    <bundle-directives></bundle-directives>
    <bundle-activator></bundle-activator>
    <bundle-activationpolicy></bundle-activationpolicy>
    <require-bundle></require-bundle>
    <service-component></service-component>
    ...
</properties>
...
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.3.4</version>
                <extensions>true</extensions>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <archive>
                        <forced>true</forced>
                    </archive>
                    <instructions>
                        <Bundle-SymbolicName>${project.artifactId}${bundle-directives}</Bundle-SymbolicName>
                        <Bundle-Name>${project.artifactId}</Bundle-Name>
                        <_nouses>true</_nouses>
                        <Class-Path>${classpath}</Class-Path>
                        <Export-Package>${export-package}</Export-Package>
                        <Import-Package>${import-package}</Import-Package>
                        <Bundle-Activator>${bundle-activator}</Bundle-Activator>
                        <Bundle-ActivationPolicy>${bundle-activationpolicy}</Bundle-ActivationPolicy>
                        <Require-Bundle>${require-bundle}</Require-Bundle>
                        <Service-Component>${service-component}</Service-Component>
                    </instructions>
                </configuration>
                <executions>
                    <execution>
                        <id>bundle-manifest</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>manifest</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            ...
        </plugins>
    </pluginManagement>
</build>

В дочерних POM мы указываем любые свойства, которые должны иметь значение, отличное от значения по умолчанию. Таких POM относительно мало в нашем случае.

Большинство наших зависимостей также не имеют манифестов OSGi, поэтому мы генерируем их как часть нашего процесса сборки. В настоящее время это выполняется скриптом Groovy, в котором используется команда BND wrap . Для большинства наших зависимостей достаточно использовать общий шаблон для манифеста. В настоящее время мы используем следующий шаблон, который генерируется на лету скриптом:

1
2
3
4
5
6
Bundle-Name: ${artifactId}
Bundle-SymbolicName: ${artifactId}
Bundle-Version: ${version}
-nouses: true
Export-Package: com.sap.*;version=${version_space},*
Import-Package: com.sap.*;version="[${version_space},${version_space}]";resolution:=optional,*;resolution:=optional

я
только в нескольких случаях шаблон манифеста должен содержать информацию, относящуюся к конкретному банку. Мы зафиксировали такие особенности, отправив эти шаблоны в нашу SCM и используя предоставленную версию вместо версии по умолчанию.

Соответствие OSGi Classloading

Альтернативы обычно используемым механизмам загрузки классов

Среды OSGi навязывают свой собственный механизм загрузки классов, который более подробно описан в следующих статьях:

Однако некоторые простые Java-приложения и библиотеки часто в значительной степени полагаются на создание пользовательских загрузчиков классов и загрузку классов с помощью Class.forName() или ClassLoader.loadClass() для использования отражения , и наше приложение было одним из них. Это проблематично в OSGi, как описано более подробно в разделе «Готовность OSGi — загрузка классов». Решения, предложенные в этой статье, хотя и действительны, но не могут быть непосредственно применены в нашем случае, поскольку это потребовало бы значительного изменения большого количества унаследованного кода, чего мы не хотели делать на данном этапе.

Мы обнаружили, что можно решить эту проблему элегантным способом, прозрачно для большей части нашего унаследованного кода, полностью полагаясь на собственные механизмы OSGi. Вместо Class.forName() можно использовать следующую последовательность вызовов:

  • Используйте FrameworkUtil.getBundle() чтобы получить текущий Bundle и его BundleContext .
  • Получить стандартную службу PackageAdmin из реестра службы OSGi через контекст пакета, полученный на предыдущем шаге
  • Используйте PackageAdmin.getExportedPackage() и ExportedPackage.getExportingBundle() чтобы найти Bundle который экспортирует пакет.
  • Наконец, просто вызовите Bundle.loadClass() чтобы загрузить запрошенный класс.

Кроме того, хотя невозможно напрямую работать с низкоуровневым загрузчиком классов пакета, сам класс Bundle предоставляет методы загрузки классов, такие как Bundle.loadClass() и Bundle.getResource() . Следовательно, можно создать собственный загрузчик классов, который упаковывает пакет (или несколько пакетов) и делегирует эти методы.

Чтобы основная часть вашего унаследованного кода работала в OSGi с незначительными изменениями, достаточно адаптировать его следующим образом:

  • Если код выполняется в OSGi, вместо вызова Class.forName() , вызовите метод, который реализует последовательность, описанную выше.
  • Если код выполняется в OSGi, вместо создания пользовательского загрузчика классов из ряда файлов BundleClassLoader создайте BundleClassLoader из комплектов, соответствующих этим файлам BundleClassLoader .

Чтобы сделать вышеуказанные изменения еще более простыми, в нашем приложении мы представили новый класс с именем ClassHelper . Это синглтон, который предоставляет следующие статические вспомогательные методы, которые делегируют идентичные нестатические методы одного экземпляра:

1
2
3
4
public static boolean isOsgi();
public static Object getBundleContext(Class&lt;?&gt; clazz);
public static Class&lt;?&gt; forName(String className, ClassLoader cl) throws ClassNotFoundException;
public static ClassLoader getBundleClassLoader(String[] bundleNames, ClassLoader cl);

Реализация по умолчанию этих методов в базовом классе ClassHelper реализует поведение по умолчанию, ClassHelper от OSGi — isOsgi() возвращает false, getBundleContext() и getBundleClassLoader() возвращают null, а forName() просто делегирует Class.forName() .

Класс OsgiClassHelper наследуется от ClassHelper и, в свою очередь, реализует правильное поведение OSGi, описанное выше. Мы поместили этот класс в его собственный специальный пакет, чтобы убедиться, что пакет, который содержит ClassHelper и большое количество других утилит, свободен от зависимостей OSGi. Этот специальный комплект имеет Activator , который заменяет экземпляр ClassHelper по умолчанию на экземпляр OsgiClassHelper при активации комплекта. Поскольку код активации выполняется только в OSGi, это обеспечивает загрузку правильной реализации в обоих случаях.

В остальной части нашего кода было достаточно просто заменить вызовы Class.forName() на ClassHelper.forName() и создание пользовательских загрузчиков классов с помощью ClassHelper.getBundleClassLoader() .

Использование OSGi Services

Во многих простых Java-приложениях определенные реализации загружаются на основе строкового «дескриптора», либо самого имени класса, либо чего-то еще. ClassLoader.loadClass() , часто в сочетании с пользовательской загрузкой классов, обычно используется для этой цели. OSGi предлагает сервисный механизм OSGi для регистрации и обнаружения таких «именованных» реализаций, который позволит вам полностью избавиться от динамической загрузки классов. Этот механизм является родным для OSGi и предлагает очень элегантную альтернативу пользовательским механизмам, упомянутым выше. Недостатком этого подхода по сравнению с подходом, представленным в предыдущем разделе, является то, что он требует несколько более глубоких изменений в вашем коде, особенно если он должен продолжать работать и за пределами OSGi.

Вам необходимо учитывать следующие аспекты:

  • Регистрация ваших интерфейсов и реализаций в реестре сервисов OSGi.
  • Обнаружение этих реализаций во время выполнения в коде, который их использует.

Хотя вы можете зарегистрировать сервисы программно, в большинстве случаев вы бы предпочли использовать подход декларативных сервисов OSGi , поскольку он позволяет зарегистрировать существующую реализацию как сервис OSGi чисто декларативным способом. Что касается обнаружения, вы можете запросить реестр служб напрямую через средства, предоставляемые BundleContext , или использовать более мощный механизм отслеживания служб .
Есть много отличных учебных пособий по сервисам OSGi и, в частности, декларативным сервисам, среди них:

В нашем случае мы не хотели слишком сильно менять нашу кодовую базу, поэтому мы переключились на сервисы OSGi лишь в нескольких местах, где, по нашему мнению, положительный эффект оправдал бы инвестиции. В настоящее время мы объявили наши существующие реализации как сервисы, добавив XML-компоненты сервисных компонентов. Хотя этот основанный на XML подход является стандартным и широко используемым, мы находим его довольно многословным и неудобным. Альтернативным подходом было бы использование аннотаций для определения компонентов и сервисов, как описано на вики-странице декларативных сервисов и в документе OSGi Release 4 . Эти аннотации уже поддерживаются BND.

Дополнительные соображения

Все пакеты экспортируются только одним комплектом

Экспорт одного и того же пакета из нескольких пакетов не работает хорошо в OSGi, поэтому его следует избегать. Если в вашем коде есть такие случаи, вы должны соответствующим образом переименовать эти пакеты.

Разоблачение точки входа OSGi

Наконец, вам может потребоваться предоставить новую точку входа для запуска приложения из консоли OSGi. Если вы используете Equinox, одним из подходящих механизмов для этого является создание приложения Equinox , которое включает в себя реализацию интерфейса org.eclipse.equinox.app.IApplication и предоставление одного дополнительного plugin.xml, как описано в разделе Начало работы с плагинами Eclipse: команда онлайн-приложения . Это приложение можно запустить из консоли Equinox OSGi с помощью команды startApp.

Вывод

Можно сделать простые Java-приложения и библиотеки совместимыми с OSGi с относительно небольшими усилиями и управляемым воздействием на существующий код, следуя рекомендациям и подходам, описанным в этом посте.

У вас есть подобный опыт создания совместимого кода Java с OSGi? Если да, я хотел бы услышать об этом.

Ссылка: Создание простого старого Java OSGi-совместимого от нашего партнера JCG Стояна Рачева в блоге Стояна Рачева .