четверг, 24 сентября 2009 г.

Введение в OSGi. Взаимодействие бандлов. События.


Наконец-то я довольно успешно сдал вступительные экзамены в аспирантуру и появилось время поделиться со своими читателями чем-то новым. В частности - выполнить свое обещание и рассказать про работу с событиями в OSGi-платформе.

Начнем с того, что в спецификации OSGi R4 определены механизмы работы с событиями, такие, как источник события - EventAdmin, обработчик события - EventHandler и непосредственно само событие - Event. Разработка диспетчера событий ложится на плечи создателей конкретной реализации OSGi-платформы. Тем самым, мы, как пользователи платформы, получаем в свое распоряжение уже готовый мощный инструмент для обеспечения взаимодействия бандлов. Осталось только научиться с ним работать.


Генерация событий


Для генерации событий служит OSGi-сервис EventAdmin. В интерфейсе EventAdmin определено два метода: postEvent - отправить событие для асинхронной обработки и sendEvent - отправить событие для синхронной обработки.

Оба этих метода в качестве параметра принимают Event - собственно событие. Каждое событие включает в себя два параметра (передаются через конструктор): топик события (String) и свойства, описывающие конкретное событие (Dictionary). Поговорим об этих параметрах подробнее.

Топик события - строка, по которой диспетчер событий сможет определить обработчик для данного события. Строка должна удовлетворять доменной модели, причем домены отделяются символом "/", например "org/beq/equinox/events/test1". Это очень важно, потому что обработчики могут подписываться сразу на группу топиков событий, используя "*" в качестве последнего домена. Например так: "org/beq/equinox/*".

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

Итак, чтобы послать событие, необходимо:
1. Получить экземпляр сервиса EventAdmin из OSGi.
2. Выбрать топик события, удовлетворяющий доменной модели.
3. Заполнить словарь (класс Dictionary) свойств события.
4. Вызвать метод sendEvent или postEvent полученного экземпляра сервиса.

Ну а теперь слайды. Создадим бандл org.beq.equinox.events.injector, в котором определим класс EventInjector, генерирующий в отдельном потоке группу событий с периодом раз в 10 секунд. В активаторе этого бандла получим EventAdmin и свяжем его с объектом класса EventInjector, после чего запустим генерацию событий.

Класс EventInjector:

package org.beq.equinox.events.injector;

import java.util.Dictionary;
import java.util.Hashtable;

import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;

public class EventInjector {

  private EventAdmin eventAdmin;

  public void inject() {
    if (getEventAdmin() != null) {
      getEventAdmin().postEvent(new Event("org/beq/equinox/events/test1", getEventProperties("1")));
      getEventAdmin().postEvent(new Event("org/beq/equinox/events/test1", getEventProperties("2")));
      getEventAdmin().postEvent(new Event("org/beq/equinox/events/test2", getEventProperties("1")));

      System.out.println("All events have been sent!");
    }
  }

  protected Dictionary<String, Object> getEventProperties(String property1Value) {
    Dictionary<String, Object> result = new Hashtable<String, Object>();
    result.put("property1", property1Value);
    return result;
  }

  public EventAdmin getEventAdmin() {
    return eventAdmin;
  }

  public void setEventAdmin(EventAdmin eventAdmin) {
    this.eventAdmin = eventAdmin;
  }

  public void start() {
    getTaskThread().start();
  }

  public void stop() {
    getTaskThread().interrupt();
  }

  private Thread taskThread = new Thread(new Runnable() {
    @Override
    public void run() {
      while (true) {
        try
        {
          inject();
          Thread.sleep(10000);
        }
        catch (InterruptedException ex) {
        }
      }
    }
  });

  protected Thread getTaskThread() {
    return taskThread;
  }
}



Активатор бандла:

package org.beq.equinox.events.injector;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.service.event.EventAdmin;
import org.osgi.util.tracker.ServiceTracker;

public class Activator implements BundleActivator {

  private EventInjector eventPublisher = new EventInjector();

  private ServiceTracker serviceTracker;

  /*
   * (non-Javadoc)
   * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
   */
  @Override
  public void start(BundleContext context) throws Exception {
    bindEventAdmin(context);
    getEventInjector().start();

    System.out.println("org.beq.equinox.events.injector has been started!");
  }

  private void bindEventAdmin(BundleContext context) {
    serviceTracker = new ServiceTracker(context, EventAdmin.class.getName(), null);
    serviceTracker.open();

    EventAdmin eventAdmin = (EventAdmin) serviceTracker.getService();
    getEventInjector().setEventAdmin(eventAdmin);
  }

  /*
   * (non-Javadoc)
   * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
   */
  @Override
  public void stop(BundleContext context) throws Exception {
    getEventInjector().stop();
    serviceTracker.close();

    System.out.println("org.beq.equinox.events.injector has been stoped!");
  }

  public EventInjector getEventInjector() {
    return eventPublisher;
  }
}



На всякий случай приведу манифест бандла:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Injector
Bundle-SymbolicName: org.beq.equinox.events.injector
Bundle-Version: 1.0.0
Bundle-Activator: org.beq.equinox.events.injector.Activator
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Import-Package: org.osgi.framework;version="1.3.0",
org.osgi.service.event;version="1.2.0",
org.osgi.util.tracker;version="1.4.2"



Обработка событий


Обработка событий осуществляется в классе, реализующем интерфейс EventHandler. Данный класс регистрируется в OSGi в качестве сервиса. При регистрации сервиса указывается словарь параметров, включающий в себя топик обрабатываемого события (обязательный параметр) и фильтр по свойствам события (необязательный параметр).

Топик события может быть строкой (String) или массивом строк (String[]). Как было отмечено выше - последним доменом в топике может быть "*", обозначающая любую подстроку. Обратите внимание, что звездочка может быть ТОЛЬКО доменом. Т.е. можно написать smth/*, но не smth/smth*. При регистрации сервиса в словаре свойств, топик имеет имя, определяемое константой org.osgi.service.event.EventConstants.EVENT_TOPIC.

Фильтр. Довольно интересная вещь, OSGi позволяет фильтровать события не только по топику, но и по их свойствам. Фильтр представляет собой LDAP-строку, например "'(&(productCategory=books)(price>=80))". Чем то напоминает LISP :) При регистрации сервиса в словаре свойств фильтр имеет имя, определяемое константой org.osgi.service.event.EventConstants.EVENT_FILTER.

Давайте снова рассмотрим пример. Создадим бандл org.beq.equinox.events.handler и определим в нем обработчик событий - класс ExampleEventHandler, который будет просто выводить в консоль параметры обрабатываемого события. В активаторе бандла зарегистрируем этот класс как сервис с разными топиками событий и параметрами фильтрации.

Обработчик событий:

package org.beq.equinox.events.handler;

import java.text.MessageFormat;

import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;

public class ExampleEventHandler implements EventHandler {

  private final String handlerName;

  @Override
  public void handleEvent(Event event) {
    System.out.println(MessageFormat.format(
        "Event Handler {0} handled event on topic {1}: Value of 'property1' = {2}",
        getHandlerName(),
        event.getProperty(EventConstants.EVENT_TOPIC),
        event.getProperty("property1")));
  }

  public ExampleEventHandler(String handlerName) {
    this.handlerName = handlerName;
  }

  protected String getHandlerName() {
    return handlerName;
  }
}



Активатор бандла:

package org.beq.equinox.events.handler;

import java.util.Dictionary;
import java.util.Hashtable;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;

public class Activator implements BundleActivator {

  /*
   * (non-Javadoc)
   * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
   */
  @Override
  public void start(BundleContext context) throws Exception {
    context.registerService(EventHandler.class.getName(),
        new ExampleEventHandler("Handler 1"),
        getHandlerServiceProperties("org/beq/equinox/events/test1"));

    context.registerService(EventHandler.class.getName(),
        new ExampleEventHandler("Handler 2"),
        getHandlerServiceProperties(new String[] { "org/beq/equinox/*" }, "(property1=2)"));

    System.out.println("org.beq.equinox.events.handler has been started!");
  }

  /*
   * (non-Javadoc)
   * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
   */
  @Override
  public void stop(BundleContext context) throws Exception {
    System.out.println("org.beq.equinox.events.handler has been stoped!");
  }

  protected Dictionary<String, Object> getHandlerServiceProperties(String... topics) {
    Dictionary<String, Object> result = new Hashtable<String, Object>();
    result.put(EventConstants.EVENT_TOPIC, topics);
    return result;
  }

  protected Dictionary<String, Object> getHandlerServiceProperties(String[] topics, String filter) {
    Dictionary<String, Object> result = new Hashtable<String, Object>();
    result.put(EventConstants.EVENT_TOPIC, topics);
    result.put(EventConstants.EVENT_FILTER, filter);
    return result;
  }
}



Манифест бандла:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Handler
Bundle-SymbolicName: org.beq.equinox.events.handler
Bundle-Version: 1.0.0
Bundle-Activator: org.beq.equinox.events.handler.Activator
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Import-Package: org.osgi.framework;version="1.3.0",
org.osgi.service.event;version="1.2.0"



Теперь нужно заставить все это работать.

Реализация диспетчера событий в Equinox и запуск бандлов


Если сейчас скомпилировать описанные выше классы в Eclipse - все скомпилируется, но работать не будет. Дело в том, что в Equinox с целью совместимости со стандартом разделены определения всех сущностей OSGi R4 и их реализация. В частности, интерфейс EventAdmin определен в бандле org.eclipse.osgi.services. Реализация же диспетчера событий находится в бандле org.eclipse.equinox.event, который можно скачать отсюда. Впрочем, начиная с Eclipse 3.6M2, реализация EventAdmin включена в Equinox SDK, поставляемую вместе с Eclipse.

Запустить созданные нами бандлы можно с помощью эклипсовского Run as OSGi Framework. В настройках запуска, на вкладке Bundles необходимо выбрать бандлы из нашего воркспейса (org.beq.equinox.event.handler и org.beq.equinox.event.injector) и необходимое окружение (бандлы org.eclipse.equinox.event, org.eclipse.osgi, org.eclipse.osgi.services, org.eclipse.osgi.util). После этого приложение можно запускать.

Вот пример того, что будет выведено в OSGi-консоль:

osgi> org.beq.equinox.events.handler has been started!
org.beq.equinox.events.injector has been started!
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 1
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 2
All events have been sent!
All events have been sent!
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 1
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 2
All events have been sent!
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 1
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 2



Стоит обратить внимание, что диспетчер событий в OSGi не сохраняет события, т.е., если бандл-обработчик еще не запущен - событие будет потеряно. Это необходимо помнить при организации правильного порядка запуска бандлов.

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

7 комментариев:

Анонимный комментирует...

Надо ли в методе stop() активатора бандра Handler разрегистрировать сервис (unregister)

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

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

Анонимный комментирует...

Извините за глупый вопрос и заранее спасибо за ответ=)
никак не получается прикрутить события к к моей View в EclipseRcp, хочется чтобы при нажатии на кнопку бандл отправлял события.

не могу понять как из активатора можно передать EventAdmin в мой View, или как получить бандл контекст во View чтобы из него получить EventAdmin.

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

Бандлы, участвующие в RCP-приложении, как правило синглетые (любой бандл, имеющий plugin.xml, должен быть синглетным, синглетность прописывается в манифесте). Соответственно, можно сохранить контекст бандла в статической переменной и возвращать статическим методом активатора

public static BundleCondext getPluginContext()
{
return _context;
}

А уже где нужно внутри бандла использовать:

Activator.getPluginContext()

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

Согласно коду я получил что и ожидал, но вывод отличается от ващего. в чём проблема: вы код поменяли перед выкладыванием или у меня окружение глючит ?

Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 1
Event Handler Handler 1 handled event on topic org/beq/equinox/events/test1: Value of property1 = 2
Event Handler Handler 2 handled event on topic org/beq/equinox/events/test1: Value of property1 = 2
All events have been sent!

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

Не совсем понял, что именно вас не устраивает? Нужно понимать, что события в приведенном коде отправляются асинхронно, поэтому порядок их обработки недетерминирован.

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

Спасибо за пост, помог решить проблему с сервисом событий.

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

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