Google Analytics

воскресенье, 18 сентября 2011 г.

Использование Spring в OSGi-контейнере

Вряд ли найдётся Java разработчик, который не знает что такое Spring Framework. Одними из базовых технологий данного фреймворка являются IoC контейнер и поддержка AOP. Эти технологии позволяют успешно разбивать архитектуру приложения на обособленные слои, как на уровне классов, так и на уровне компоновки объектов во время выполнения. Казалось бы, что приложение отлично структурировано на элементы/слои, но по своей сути оно остаётся монолитным. Монолитным во время выполнения (runtime)! Только в рамках данного фреймворка не существует универсальных решений этой проблемы. Чуть меньшее количество Java разработчиков слышало об OSGi. Это спецификация модульных систем для Java платформы. Использование конкретной реализации данной спецификации в качестве основы приложения позволяет сделать его модульным, как во время выполнения, так и на физическом уровне (уровне файлов). О синергии этих технологий и пойдёт речь в этой статье.

Spring Framework

Я рассчитываю, что читатель знаком с данным фреймворком хотя бы на уровне прочтения первого десятка глав из официального руководства. Не представляется возможным описать всё это в одной статье. Поэтому отсылаю всех, кому это нужно, к официальному руководству. Ниже приведу лишь краткую справку по основным компонентам Spring Framework.
  • IoC контейнер с реализацией паттерна DI - механизм управляющий созданием, конфигурированием и связыванием объектов во время выполнения, базовая неотъемлемая часть фреймвока.
  • AOP - поддержка механизмов аспекнто-ориентированного программирования. Базовый механизм, который используют множество подсистем.
  • Интеграция с источниками данных, поддержка ORM и поддержка транзакций - механизмы обеспечивающие взаимодействие с различными источниками данных от XML до СУБД, интеграцию со сторонними фреймворками в данных областях, поддержка локальных и глобальных транзакций, трансляция специфичных иерархий исключений доступа к источникам данных в собственную иерархию и прочее.
  • Web компоненты - обеспечивают различные механизмы от собственной реализации паттерна MVC до интеграции с различными фреймворками связанными с Web (JSF, Struts, WS и прочее).
Множество различных субпроектов из портфолио SpringSource.

Модульность

В идеале, действительно модульная программа должна состоять из элементов, которые обладают следующими ярко выраженными признаками: слабая связанность (взаимодействие через чётко выраженный интерфейс, лёгкая заменяемость, многократное использование), инкапсуляция (модуль рассматривается снаружи как чёрный ящик с определенным интерфейсом взаимодействия), динамичность (возможность изменять множество модулей во время выполнения). Java позволяет реализовать слабую связанность и инкапсуляцию, как на уровне классов, так и на уровне пакетов. Конечно есть способы нарушить инкапсуляцию на уровне модификаторов доступа (private, protected) с помощью рефлексии (Reflection API) и тем самым связать свой код с внутренними механизмами стороннего кода, либо использовать механизмы изменения кода классов (javassist, cglib, изменения кода классов вручную и т.д.), но это скорее хаки. А вот что на счёт динамичности, то есть возможности заменять модули во время выполнения. В данной области дела обстоят хуже. Можно конечно реализовать поддержку динамичности переопределяя загрузчики классов (ClassLoader) своими реализациями, но это очень большой объём работы, который за нас уже проделали разработчики реализаций стандарта OSGi. Любой контейнер OSGi уже поддерживает модульность на уровне базовой архитектуры и API.

OSGi

Open Services Gateway initiative - спецификация имеющая множество реализаций. Основные Open Source реализации это Apache Felix, Equinox и Knopflerfish. Данная спецификация описывает модульную систему, которая динамически может связывать различные модули (bundles). Состав модулей может изменяться во время выполнения. Взаимодействие между модулями осуществляется с помощью сервисов, которые зарегистрированы в регистре сервисов (Service Register). Модули обладают жизненным циклом, который состоит из нескольких состояний (INSTALLED, RESOLVED, STARTING, ACTIVE, STOPPING, UNINSTALLED). Жизненным циклом модуля управляет OSGi-контейнер.


Диаграмма состояний OSGi-модуля

OSGi-модуль (bundle) должен иметь дополнительные метаданные в файле META-INF/MANIFEST.MF. Некоторые из основных представлены ниже:
  • Bundle-Name - удобочитаемое имя модуля;
  • Bundle-Version - версия модуля в формате число[.число[.число[.строка]]], по-умолчанию версия 0.0.0;
  • Bundle-SymbolicName - символьное имя модуля, совместно с версией служат уникальным идентификаторов модуля;
  • Export-Package - список экспортируемых Java-пакетов модуля с возможными дополнительными директивами;
  • Import-Package - список импортируемых Java-пакетов используемых в модуле с возможными дополнительными директивами.

Как появилась возможность использовать Spring вместе с OSGi

OSGi реализации позволяют создавать действительно модульные программы на Java. Spring Framework даёт возможность избавиться от ручной поддержки связей между объектами благодаря использованию IoC контейнера, расширить функционал существующих классов с помощью AOP, и использовать любые сопутствующие технологии из портфолио SpringSource. Объединение этих технологий должно приносить немалую пользу при разработке программного обеспечения. Неудивительно, что эта мысль пришла в голову нескольким людям из Interface21 (позднее SpringSource) в 2006 году и они смогли подключить к разработке людей из OSGi Alliance, BEA, Oracle, IBM. В результате совместных усилий появился продукт Spring dm под крылом SpringSource, позже этот продукт дал жизнь спецификации Blueprint в OSGi Service Platform Service Compendium. Его кодовая база перешла под опеку Eclipse Foundation и получила название Gemini Blueprint. SpringSource также выпускала dm Server, теперь его кодовая база стала проектом Virgo в Eclipse Foundation.

Архитектура Gemini Blueprint (Spring DM)

Каким же образом Gemini Blueprint позволяет использовать Spring Framework в OSGi-контейнере? Для ответа на этот вопрос необходимо рассмотреть основные моменты архитектуры Gemini Blueprint.

Базовым элементом архитектуры является extender (реализация extender паттерна OSGi). Extender отслеживает события запуска новых модулей (bundle) в сервисной платформе OSGi и проверяет поддерживает ли данный модуль спецификацию blueprint. В случае поддержки модулем данной спецификации, extender создает Spring-контекст модуля и инициализирует его. Таким образом каждый Spring-модуль имеет собственный Spring-контекст. В добавок, из extender вызывается код, который ответственен за публикацию Spring-bean в качестве сервисов OSGi и получения ссылки на сервисы OSGi, и прочее.


Схема функционирования extender

Всю информацию на базе которой extender создаёт контекст берётся из конфигурации в виде xml-файлов. Здесь необходимо подчеркнуть существование двух возможных вариантов конфигурации spring и blueprint. Оба варианта во многом схожи, но если первый соответствует родной Spring конфигурации с дополнительными именованными пространствами для конфигурирования сервисов, то второй чётко определён спецификацией Blueprint OSGi Service Platform Service Compendium. На самом деле есть ещё и третий вариант, обратно совместимый с Spring dm.

Для более подробного изучения архитектуры смотрите ссылки в конце статьи.

Пример использования Gemini Blueprint и OSGi

В качестве примера использования, напишем GUI приложение с двумя реализациями сервисов и одним клиентом. Этот пример продемонстрирует насколько использование Gemini Blueprint облегчает разработку OSGi-программ. Во-первых, нам не понадобится использование OSGi API. Во-вторых, вопрос динамичности сервисов решается самостоятельно Gemini Blueprint, нам лишь останется позаботиться только о логике приложения. В-третьих, мы будем использовать Spring Framework для компоновки приложения.

Сборка приложения осуществляется с помощью maven 3. Файловая структура проекта выглядит следующим образом:
blueprint-example
├── consumer
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── blueprint
│       │   │       └── example
│       │   │           └── consumer
│       │   │               ├── ConsumerFrame.java
│       │   │               ├── Consumer.java
│       │   │               └── RefreshListener.java
│       │   └── resources
│       │       └── META-INF
│       │           └── spring
│       │               ├── osgi-context.xml
│       │               └── spring-context.xml
│       └── test
│           ├── java
│           └── resources
├── date-producer
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── blueprint
│       │   │       └── example
│       │   │           └── dateproducer
│       │   │               └── ProducerImpl.java
│       │   └── resources
│       │       └── META-INF
│       │           └── spring
│       │               ├── osgi-context.xml
│       │               └── spring-context.xml
│       └── test
│           ├── java
│           └── resources
├── int-producer
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── blueprint
│       │   │       └── example
│       │   │           └── intproducer
│       │   │               └── ProducerImpl.java
│       │   └── resources
│       │       └── META-INF
│       │           └── spring
│       │               ├── osgi-context.xml
│       │               └── spring-context.xml
│       └── test
│           ├── java
│           └── resources
├── pom.xml
└── producer-api
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── blueprint
        │   │       └── example
        │   │           └── producer
        │   │               └── api
        │   │                   └── Producer.java
        │   └── resources
        │       └── META-INF
        └── test
            ├── java
            └── resources

В директориях consumer, producer-api, date-producer и int-producer расположены соответственно проекты клиента, API сервиса, реализации сервисов на основе даты и целого числа. Звучит запутанно? Не волнуйтесь, сейчас разберём всё по порядку.

Каждый из перечисленных проектов представляет собой обособленный модуль (bundle) для OSGi-контейнера. Проект producer-api содержит всего один файл с интерфейсом Producer. Это типичный пример выделения API OSGi-приложения в отдельный модуль, который будут импортировать все нуждающиеся в нём модули. Этот модуль не является blueprint-модулем, потому что его цель только в обеспечении необходимыми классами.

Для сборки всех модулей используется maven-bundle-plugin. Это плагин от разработчиков Apache Felix, который использует внутри код (знаменитой в OSGi кругах) утилиты bnd. Этот плагин конфигурируется следующим образом:

<plugin>
    <groupid>org.apache.felix</groupid>
    <artifactid>maven-bundle-plugin</artifactid>
    <version>2.3.5</version>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            <bundle-name>
                ${name} ${version}
            </bundle-name>
            <bundle-symbolicname>
                ${groupId}.${artifactId}
            </bundle-symbolicname>
            <export-package>
                blueprint.example.producer.api
            </export-package>
            <import-package>*</import-package>
        </instructions>
    </configuration>
</plugin>

Внимательный читатель должен заметить, что наименование записей в секции instructions совпадают с наименованиями специфических заголовков для метаданных OSGi-модуля в META-INF/MANIFEST.MF. Это они и есть, только не в своём натуральном виде, а в виде инструкций bnd. Эта утилита достаточно интеллектуальна и умеет вычислять зависимости по коду и подставлять их в секцию Import-Package, а также добавлять различные метаданные. На основе своей конфигурации и вычислений она генерирует MANIFEST.MF. В данном случае, мы определяем только основные заголовки.

Для того, чтобы extender распознал модуль как blueprint-модуль, по-умолчанию, используется следующее правило: модуль должен иметь xml файлы конфигурации в директории META-INF/spring, либо должен присутствовать заголовок Spring-Context указывающий на месторасположение конфигурации. В проектах перечисленных выше используются по два файла конфигурации в директории META-INF/spring. Первый с префиксом "osgi-" содержит конфигурацию специфичную для blueprint. Второй с префиксом "spring-" содержит обычную конфигурацию для spring-приложения.

Рассмотрим реализацию сервисов на основе даты и на основе произвольного целого числа:

@Component
public class ProducerImpl implements Producer {

    @Override
    public String produceString() {
        return new Date().toString();
    }

}

@Component
public class ProducerImpl implements Producer {
    
    private Random random = new Random(new Date().getTime());
    
    @Override
    public String produceString() {
        return String.valueOf(random.nextInt());
    }

}

Сервис в нашем учебном примере это всего лишь класс с одним методом String produceString(), который возвращает произвольную строку. В первом случае локализованную дату, во втором строку с произвольным целым числом. В spring-конфигурации данных модулей задаётся процесс сканирования аннотированных классов. В osgi-конфигурации регистрируется сервис.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    ">    
    
    <context:annotation-config/>
    <context:component-scan base-package="blueprint.example"/>
    
</beans>

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.eclipse.org/gemini/blueprint/schema/blueprint"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:beans="http://www.springframework.org/schema/beans"                 
   xsi:schemaLocation="
       http://www.eclipse.org/gemini/blueprint/schema/blueprint 
       http://www.eclipse.org/gemini/blueprint/schema/blueprint/gemini-blueprint.xsd
       http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       ">
    
    <service id="producerService" ref="producerImpl" 
    interface="blueprint.example.producer.api.Producer"/>
</beans:beans>

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

Настало время рассмотреть последний модуль. Клиент наших сервисов выводит на экран GUI форму с текстовым полем отображения и кнопкой обновления значения. Давайте взглянем на код клиента.

@Component
public class Consumer {
    
    @Autowired
    private ConsumerFrame consumerFrame;    
    @Autowired
    private Producer producer;

    @PostConstruct
    public void start() {
        consumerFrame.setRefreshListener(new RefreshListener() {            
            @Override
            public String refresh() {
                return producer.produceString();
            }
        });
        
        SwingUtilities.invokeLater(
                new Runnable() {
                    
                    @Override
                    public void run() {
                        consumerFrame.setVisible(true);
                    }
                }
                );
    }
    
}

Класс Consumer очень простой. В нём находятся два атрибута, которые инжектируются контейнером. Это объект формы и ссылка на наш сервис. Далее в методе, который вызывается после конструирования объект, регистрируется слушатель события обновления с кодом обращения к сервису и код отображения формы на экран. Spring-конфигурация данного проекта выглядит идентично представленной выше, а osgi-конфигурация выглядит так:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.eclipse.org/gemini/blueprint/schema/blueprint"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:beans="http://www.springframework.org/schema/beans"
   xsi:schemaLocation="
       http://www.eclipse.org/gemini/blueprint/schema/blueprint 
       http://www.eclipse.org/gemini/blueprint/schema/blueprint/gemini-blueprint.xsd
       http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       ">
    
    <reference id="producer" 
    interface="blueprint.example.producer.api.Producer"/>
    
</beans:beans>


В данной конфигурации регистрируется ссылка на сервис. По-умолчанию, данный сервис является обязательным, и все вызовы к нему, при его отсутствии, будут блокируемыми на определенный интервал, по истечении которого будет возникать исключение. Это поведение полностью поддаётся конфигурированию.

У программистов знакомых со Spring Framework наверняка возник вопрос. Каким образом ссылка на сервис обеспечивает динамичность этого сервиса? Ведь все объекты конфигурируются на этапе загрузки (eager), либо на этапе обращения (lazy). Что же будет если ссылка producer утратит свою актуальность по причине исчезновения сервиса. Ответ: blueprint заботится об этом. На самом деле в качестве ссылки на сервис инжектируется прокси-объект, который и обслуживает динамичность. Этот объект обеспечивает поведение блокирования вызова, а также генерирование исключения.

В архиве с исходным кодом, помимо него присутствует директория с настроенным контейнером equinox и всеми необходимыми библиотеками (equinox-for-example). Думаю это облегчит быстрый старт. Для запуска контейнера необходимо зайти в директорию с ним и выполнить команду:

java -jar org.eclipse.osgi_3.6.2.R36x_v20110210.jar -console
После неё на экране появятся сообщения от инициализирующихся модулей. И в итоге приглашение командной строки equinox. После ввода команды ss вывод должен быть идентичным представленному ниже:

osgi> ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.6.2.R36x_v20110210
1       ACTIVE      com.springsource.net.sf.cglib_2.2.0
2       ACTIVE      com.springsource.org.aopalliance_1.0.0
3       ACTIVE      com.springsource.org.apache.log4j_1.2.16
4       ACTIVE      com.springsource.slf4j.api_1.6.1
                    Fragments=5
5       RESOLVED    com.springsource.slf4j.log4j_1.6.1
                    Master=4
6       ACTIVE      com.springsource.slf4j.org.apache.commons.logging_1.6.1
7       ACTIVE      org.eclipse.gemini.blueprint.core_1.0.0.RELEASE
8       ACTIVE      org.eclipse.gemini.blueprint.extender_1.0.0.RELEASE
9       ACTIVE      org.eclipse.gemini.blueprint.io_1.0.0.RELEASE
10      ACTIVE      org.springframework.aop_3.0.6.RELEASE
11      ACTIVE      org.springframework.asm_3.0.6.RELEASE
12      ACTIVE      org.springframework.aspects_3.0.6.RELEASE
13      ACTIVE      org.springframework.beans_3.0.6.RELEASE
14      ACTIVE      org.springframework.context_3.0.6.RELEASE
15      ACTIVE      org.springframework.context.support_3.0.6.RELEASE
16      ACTIVE      org.springframework.core_3.0.6.RELEASE
17      ACTIVE      org.springframework.expression_3.0.6.RELEASE


Это список всех модулей установленных на текущий момент. Теперь необходимо установить тестовое приложение. Для этого вызовите команду install file:///путь_к_jar_файлу_модуля для каждого из 4-х модулей (собрать модули можно maven-командой package). После этого запустите командой start id на выполнения модули с API, один из сервисов и клиента. На экране должно появиться окно тестового приложения.


Использование date-producer

Затем остановите командой stop id модуль сервиса и запустите другой сервис. Обновите содержимое текстового поля окна, нажав кнопку refresh.


Использование int-producer

На этом описание тестового примера закончено. Хочу подчеркнуть, что это только малая часть возможностей Gemini Blueprint.


Область применения

OSGi давно стал нечто большим нежели стандарт модульных систем для встраиваемой техники. В подтверждение вышесказанного достаточно привести пример таких широко распространённых проектов основанных на OSGi, как Eclipse и Glassfish. Spring Framework имеет уверенное положение на корпоративном рынке. Для чего же может быть использована синергия этих технологий? Основной областью применения этой связки, по задумке создателей, должна была стать область корпоративных приложений. Собственно это успешная попытка привнесения OSGi в корпоративный мир. И теперь мы имеем возможность использовать OSGi в любых корпоративных и прикладных приложениях совместно со Spring Framework.

Недостатки

Не смотря на большие преимущества, которыми сулит динамическая модульная архитектура, у связки Spring и OSGi есть свои недостатки. Наиболее существенным недостатком я считаю сравнительно малую распространённость данной связки. Конечно по отдельности эти технологии не вызывают сомнений в своей живучести, но количество пользователей использующих их вместе не так велико. Субъективно, процесс передачи проектов от SpringSource под крыло Eclipse Foundation замедлил их развитие. Вторым недостатком можно назвать дополнительную сложность, которую добавляют к проекту использование данной связки. И это ни столько сложность использования данных технологий, сколько сложность добавляемая проблемами совместимости со сторонними библиотеками, вернее проблемами использования их в OSGi контейнере.


Проекты связанные с применением Spring в OSGi контейнере

Spring DM - проект предназначенный для облегчения использования Spring-приложений в OSGi-контейнере. В данный момент не развивается (см. Eclipse Gemini).
Spring DM Server - полностью модульный сервер приложений Java, разработанный для запуска корпоративных приложений и Spring-приложений. Обладет высокой степенью гибкости и надёжности. В данный момент не развивается (см. Eclipse Virgo).
Gemini Blueprint - наследник Spring DM. Проект позволяет запускать Spring-приложения в OSGi-контейнере.
Gemini Web - базируется на референсной реализации спецификации Web Applications от OSGi Alliance. Проект для запуска Spring Web-модулей в OSGi-контейнере.
Gemini JPA - модульная реализация Java Persistence API для OSGi-контейнера. На данный момент обеспечивает интеграцию c JPA провайдером EclipseLink.
Gemini DBAccess - обеспечивает распространение JDBC-драйверов подходящих для запуска в OSGi-контейнере и механизмы для их использвоания.
Gemini Management - проект для удалённого управления модульной системой с использованием средств JMX.
Gemini Naming - поддержка JNDI в OSGi-контейнере.
Eclipse Virgo - OSGi сервер приложений, наследник Spring DM Server. Поддерживает следующие виды форматов развёртывания (deployment formats): OSGi модули (bundle), Java EE WAR, Web-модули (Web-bundles), PAR (подобие EAR для Java EE, содержит архивы компонентов), Plan (конфигурация нескольких модулей в общее приложение), Configuration (механизм динамического обновления конфигурации приложения). Содержит механизм горячего развёртывания приложений, панель администрирования, дополнительные библиотеки. Может поставляться с двумя servlet-контейнерами Tomcat и Jetty. А также множество дополнительного функционала.

Заключение

Я задумывал статью как вводное знакомство с технологией совместного использования Spring и OSGi. Для того чтобы полностью описать всю информацию по вводной части мне потребовался бы гораздо больший объём, поэтому предлагаю заинтересованному читателю самостоятельно изучить дополнительные источники. К тому же, я буду рад ответить на вопросы по этой замечательной технологии (в границах своей компетентности).

Дополнительные источники

Архив с примером
Eclipse Gemini
Bnd
OSGi Specifications
Spring Framework
Spring Dynamic Modules
SpringSource Blog about OSGi
Spring Dynamic Modules In Action
Eclipse Virgo


4 комментария:

  1. Привет,
    почитал эту статью на хабре, очень понравилась статья, особенно в свете того что я как раз искал информацию по данной теме, в связи с желанием перевести небольшой проект на OSGi, единственное я не совсем понял несколько вещей:
    1. как я понимаю любая документация по SpringDM актуальна для Eclipse Gemini?
    2. был ли у вас опыт использования внутри одного из модулей Hibernate и если да, то насколько нормально он себя ведет?
    3. есть ли возможность подружить CXF (SOAP, REST) с этой кухней

    ОтветитьУдалить
  2. Привет!
    1. По большей части да, но необходимо учитывать отличия в версии spring dm 2 на кодовой базе которой и начал развиваться проект Eclipse Gemeni.
    2. Небольшой опыт есть. Я планирую написать статью об этом в ближайший месяц. На данный момент можете посмотреть на мою реализацию Hibernate Extender http://code.google.com/p/iconcerto/source/browse/#hg%2Fhibernate-extender%253Fstate%253Dclosed
    3. Никогда не использовал CXF, но думаю проблем быть не должно. Обратите внимание на поддержку веб технологий на базе OSGi. В проекте, ссылка на который указана выше, можно найти пример использования сервлет-контейнера (wiki).

    ОтветитьУдалить
  3. Привет еще небольшой вопрос,
    а как это все разрабатывается? я правильно понимаю что код пишется в eclipse, после чего для проверки как все работает надо собирать jar файл и запускать его на контйнере?

    ОтветитьУдалить
  4. В проекте, на который я ссылался выше, сборка осушествляется с помощью maven. Код пишется в любой удобной IDE или редакторе кода. Можно конечно собирать bundle и запускать его в контейнере во время разработки вручную, но лучше использовать интеграционные тесты http://code.google.com/p/iconcerto/source/browse/#hg%2Fintegration-tests

    ОтветитьУдалить