вторник, 12 мая 2009 г.

Введение в OSGi. Пример использования декларативных сервисов


В прошлой заметке мы познакомились с парадигмой декларативных сервисов. Пришло время рассмотреть пример их использования. Напомню, что мы создали бандл org.beq.equinox.ds.intro, выставляющий сервис SampleRunnable, который умеет стартовать поток и печатать приветствие. Давайте создадим клиент для этого сервиса.

Клиент будет расширять консоль OSGi. Как это делается хорошо написано в статье Explore Eclipse's OSGi console. Если говорить коротко: необходимо реализовать CommandProvider в методе getHelp() описать справочную строку, а в методах _команда - определить логику команд. Т.е. идея такая: мы напишем бандл, который предоставляет сервис, реализующий CommandProvider. В свою очередь данный сервис использует сервис SampleRunnable из бандла org.beq.equinox.ds.intro. Все сервисы будут декларативными.


Создадим PDT/Plug-in Project с названием org.beq.equinox.ds.sampleprovider1. Создадим класс SampleCommandProvider. Код класса следующий:

package org.beq.equinox.ds.sampleprovider1;



import org.eclipse.osgi.framework.console.CommandInterpreter;

import org.eclipse.osgi.framework.console.CommandProvider;



/**

 *  

 * @author psamolisov

 * @since  11.05.2009

 */


public class SampleCommandProvider implements CommandProvider

{

    private Runnable runnable;

   

    public synchronized void setRunnable(Runnable r) {

        runnable = r;

    }

   

    public synchronized void unsetRunnable(Runnable r) {

        runnable = null;

    }

   

    public synchronized void _run(CommandInterpreter ci) {

        if (runnable != null) {

            runnable.run();

        } else {

            ci.println("Error, no Runnable available");

        }

    }

   

    @Override

    public String getHelp()

    {

        return "\trun - execute a Runnable service";

    }

}

 


Т.е. мы реализуем dependency injection - переменная runnable будет установлена с помощью OSGi-среды по правилам, указанным в дескрипторе развертывания сервиса. Кстати, надо бы его создать. По старой привычке создаем каталог /OSGI-INF/ в корне проекта, а в каталоге - файл commandprovider1.xml следующего содержания:

<?xml version="1.0"?>

<component name="commandprovider1">

    <implementation class="org.beq.equinox.ds.sampleprovider1.SampleCommandProvider"/>

    <service>

        <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>

    </service>

    <reference name="RUNNABLE"

       interface="java.lang.Runnable"

       bind="setRunnable"

       unbind="unsetRunnable"

       cardinality="0..1"

       policy="dynamic"/>

</component>


Давайте разберемся что здесь есть что. Тег component содержит в себе описание декларативного сервиса. Тег implementation указывает имя класса, реализующего сервис. Собственно тег service указывает DS зарегистрировать компонент как сервис. provide - указывает имя интерфейса, под которым сервис будет зарегистрирован в реестре сервисов OSGi.

Тег reference описывает зависимости нашего сервиса от других сервисов. Атрибут name определяет уникальное имя зависимости, атрибут interface - интерфейс, реализуемый нужным нам сервисом, в данном случае - java.lang.Runnable. bind - определяет метод, вызываемый для регистрации зависимости, т.е. для осуществления dependency injection. unbind - наоборот, говорит нам, какой метод будет вызван для удаления зависимости. Самыми интересными являются атрибуты cardinality и policy.

Атрибут cardinality определяет параметры зависимости: принудительная или опциональная, исключительная или многократная. Может принимать одно значение из следующих вариантов:
- 0..1 - опциональная, единственная. Соответствует отношению "ноль или один".
- 1..1 - принудительная, единственная. Соответствует отношению "исключительно один".
- 0..n - опциональная, множественная. Соответствует отношению "ноль ко многим".
- 1..n - принудительная, множественная. Соответствует отношению "один ко многим".

В нашем случае мы используем отношение "0..1", это значит, что если не будет доступно ни одного сервиса Runnable - будет выведено сообщение Error, no Runnable available. Если поменять значение атрибута на "1..1", то в случае недоступности Runnable при старте нашего сервиса мы получим ошибку. При попытке выполнить команду run в консоли OSGi нам скажут, что данная команда недоступно. Это как раз свидетельствует о том, что наш сервис не зарегистрирован в реестре сервисов.

Атрибут policy может принимать два значения: dinamyc или static. Значением по-умолчанию является static. Определяет будет ли компонент поддерживать динамическое переключение сервисов. Если не будет, то при обновлении сервисов-зависимостей будет необходимо рестартовать наш бандл, что чревато накладными расходами. Поэтому есть смысл присваивать этому атрибуту значение dynamic.

Не забываем зарегистрировать дескриптор сервиса в манифесте бандла. Манифест бандла будет выглядить следующим образом:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: Sampleprovider1 Plug-IN

Bundle-SymbolicName: org.beq.equinox.ds.sampleprovider1

Bundle-Version: 1.0.0

Bundle-Activator: org.beq.equinox.ds.sampleprovider1.Activator

Require-Bundle: org.eclipse.equinox.ds,

 org.eclipse.osgi,

 org.eclipse.equinox.util

Service-Component: OSGI-INF/commandprovider1.xml

 


Запускаем бандлы org.beq.equinox.ds.sampleprovider1 и org.beq.equinox.ds.intro с помощью Eclipse. Посмотрим состояние OSGi-шины:

osgi> ss



Framework IS launched.



id  State       Bundle

...

188 ACTIVE      org.beq.equinox.ds.intro_1.0.0

1190    ACTIVE      org.beq.equinox.ds.sampleprovider1_1.0.0



osgi>


Как видим оба наши бандла находятся в состоянии ACTIVE. Выполним определенную нами команду run:

osgi> run

Hello FROM SampleRunnable



osgi>


Сервис CommandProvider смог найти зарегистрированный в реестре серсив Runnable и обратиться к нему. Теперь остановим бандл org.beq.equinox.ds.intro и заново попытаемся выполнить команду run:

osgi> stop 188

stop SampleActivator!



osgi> run

Error, no Runnable available



osgi>


Что и следовало ожидать - ни один Runnable не найден, следовательно выводим предупреждение.

Давайте теперь попробуем создать сервис, обеспечивающий взаимодействие со множеством Runnable. Создадим бандл org.beq.equinox.ds.sampleprovider2, в котором определим сервис SampleCommandProvider. Код сервиса будет следующим:

package org.beq.equinox.ds.sampleprovider2;



import java.util.ArrayList;

import java.util.Collections;

import java.util.List;



import org.eclipse.osgi.framework.console.CommandInterpreter;

import org.eclipse.osgi.framework.console.CommandProvider;





/**

 *  

 * @author psamolisov

 * @since  11.05.2009

 */


public class SampleCommandProvider implements CommandProvider

{

    private List<Runnable> runnables = Collections.synchronizedList(new ArrayList<Runnable>());

   

    public void addRunnable(Runnable r) {

        runnables.add(r);

    }

   

    public void removeRunnable(Runnable r) {

        runnables.remove(r);

    }

   

    public void _runall(CommandInterpreter ci) {

        synchronized(runnables) {

            for (Runnable r : runnables) {

                r.run();

            }

        }

    }

   

    @Override

    public String getHelp()

    {

        return "\trunall - Run all registered Runnables";

    }

}

 


Как видим - изменения небольшие. Введен список Runnable и методы добавления и удаления элементов из списка.

Дескриптор сервиса:

<?xml version="1.0"?>

<component name="commandprovider2">

    <implementation class="org.beq.equinox.ds.sampleprovider2.SampleCommandProvider"/>

    <service>

        <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>

    </service>

    <reference name="RUNNABLE"

       interface="java.lang.Runnable"

       bind="addRunnable"

       unbind="removeRunnable"

       cardinality="0..n"

       policy="dynamic"/>

</component>


Ну и для затравки - манифест бандла:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: Sampleprovider2 Plug-IN

Bundle-SymbolicName: org.beq.equinox.ds.sampleprovider2

Bundle-Version: 1.0.0

Bundle-Activator: org.beq.equinox.ds.sampleprovider2.Activator

Require-Bundle: org.eclipse.equinox.ds,

 org.eclipse.osgi,

 org.eclipse.equinox.util

Service-Component: OSGI-INF/commandprovider2.xml

 


Давайте запустим данный бандл:
osgi> ss



Framework IS launched.



id  State       Bundle

...

188 ACTIVE      org.beq.equinox.ds.intro_1.0.0

1190    ACTIVE      org.beq.equinox.ds.sampleprovider1_1.0.0

1195    ACTIVE      org.beq.equinox.ds.sampleprovider2_1.0.0


Выполним команду runall:

osgi> runall

Hello FROM SampleRunnable



osgi>


Как видим, наш Runnable успешно зарегистрировался в новом сервисе SampleCommandProvider. Давайте остановим бандл org.beq.equinox.ds.intro и снова выполним команду runall:

osgi> stop 188

stop SampleActivator!



osgi> runall



osgi>


Ничего не произошло. Когда мы остановили бандл org.beq.equinox.ds.intro выполнился метод removeRunnable и наш единственный Runnable был удален из списка.

Думаю, такие простые примеры помогут понять основные концепции использования декларативных сервисов. Ничего сложного здесь нет, все это очень похоже на тот же Spring - декларативное описание зависимостей, только зависимости эти находятся в разных бандлах. К слову, Spring Source создали так называемые Spring Modules - OSGi бандлы с использованием всех возможностей Spring Framework.

З.Ы. Примеры исходного кода взяты отсюда.

UPD: 16.10.09. Небольшое дополнение: декларативные сервисы создаются по цепочке. Чтобы начали разрешаться зависимости - надо вызвать сервис-клиент через активатор (напрямую или через другой декларативный сервис). Полностью уйти от Java-кода, к сожалению, не удастся.

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

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

Coffius комментирует...

Спасибо за хорошую статью. После нее решил использовать equinox в своем проекте, но обнаружил одну проблему с equinox: если в методе BundleActivator.start() при автоматическом старте из eclipse происходит exception, то в консоль equinox-а не выводится никакой информации об ошибке. Выводится только при попытке ручного запуска бандла(командой "start"). Можно ли это как-нибудь исправить?

Samolisov Pavel комментирует...

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

Seeker комментирует...

Павел, скажи те пожалуйста, в манифест бандла вы указываете
Bundle-Activator: org.beq.equinox.ds.sampleprovider1.Activator, но он не указан пример кода к этому активатору, быть может я что то упустил, тогда прошу поправить меня, и еще один вопрос, если аналог CommandProvider не для quinox, а для Apache Felix?

Pavel Samolisov комментирует...

Здравствуйте.
org.beq.equinox.ds.sampleprovider1.Activator - тривиальный активатор, который только выводит на консоль информацию о том, что бандл запущен или остановлен.

По поводу Apache Felix ответить не могу, не компетентен.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!