вторник, 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

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

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

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

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

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

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

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