В прошлой заметке мы познакомились с парадигмой декларативных сервисов. Пришло время рассмотреть пример их использования. Напомню, что мы создали бандл 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";
}
}
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 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
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>
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>
Hello FROM SampleRunnable
osgi>
Сервис CommandProvider смог найти зарегистрированный в реестре серсив Runnable и обратиться к нему. Теперь остановим бандл org.beq.equinox.ds.intro и заново попытаемся выполнить команду run:
osgi> stop 188
stop SampleActivator!
osgi> run
Error, no Runnable available
osgi>
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";
}
}
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>
<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
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
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>
Hello FROM SampleRunnable
osgi>
Как видим, наш Runnable успешно зарегистрировался в новом сервисе SampleCommandProvider. Давайте остановим бандл org.beq.equinox.ds.intro и снова выполним команду runall:
osgi> stop 188
stop SampleActivator!
osgi> runall
osgi>
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
Дело в том, что информация о старте бандлов не выводится в консоль эклипса. Т.е. сделано следующим образом: сначала стартуют бандлы, а потом выводится приглашение командной строки OSGi-консоли. Думаю что-либо сделать здесь можно только исправив исходники эклипса. Увы.
ОтветитьУдалитьПавел, скажи те пожалуйста, в манифест бандла вы указываете
ОтветитьУдалитьBundle-Activator: org.beq.equinox.ds.sampleprovider1.Activator, но он не указан пример кода к этому активатору, быть может я что то упустил, тогда прошу поправить меня, и еще один вопрос, если аналог CommandProvider не для quinox, а для Apache Felix?
Здравствуйте.
ОтветитьУдалитьorg.beq.equinox.ds.sampleprovider1.Activator - тривиальный активатор, который только выводит на консоль информацию о том, что бандл запущен или остановлен.
По поводу Apache Felix ответить не могу, не компетентен.