суббота, 21 марта 2009 г.

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


Hello, суровый челябинский программист here. И мы вновь продолжаем наше знакомство с OSGi. В предыдущей заметке мы говорили о взаимодействии бандлов посредством зависимостей. Однако, в OSGi существует и другой - более гибкий - способ обеспечить совместную работу бандлов и имя ему - сервисы.

Впрочем, предлагаю перейти сразу к делу. На рисунке представлена схема взаимодействия сервисов. Один Java-объект, который называется Service, представляет некий интерфейс доступа к себе - контракт сервиса. Другой Java-объект, который называется Client, может взаимодействовать с сервисом, через предоставляемый тем контракт.



Чтобы данное взаимодействие было возможно Client должен найти нужный ему Service и получить к нему доступ. Для этого используется Service Broker, в роли которого предстает OSGi-реестр сервисов. Бандл, предоставляющий сервис, регистрирует его в реестре, а бандл-клиент извлекает сервис оттуда и использует.


Теперь займемся практикой. Создадим бандл org.beq.equinox.service, который будет выставлять ColorizerService - сервис, предоставляющий некую палитру цветов.

Для начала определим интерфейс IBeqService и договоримся, что все наши сервисы будут его реализовывать. Данный интерфейс определяет один метод - getName(), позволяющий идентифицировать сервисы в системе.

Код интерфейса:

package org.beq.equinox.service;



public interface IBeqService

{

    public String getName();

}

 


Теперь напишем сам класс сервиса. Не будем сильно заморачиваться - определим лишь метод listColors, который возвращает массив цветов:

package org.beq.equinox.service;



public class ColorizerService implements IBeqService

{

    private static final String NAME = ColorizerService.class.getName();

   

    @Override

    public String getName()

    {

        return NAME;

    }



    public String[] listColors()

    {

        return new String[] {

                "Red", "Green", "White", "Black"

        };

    }

}

 


Все, сервис готов. Теперь необходимо зарегистрировать сервис в реестре сервисов. Сделать это можно (да и нужно) в активаторе бандла, предоставляющего сервис. В нашем случае - в классе Activator бандла org.beq.equinox.service.

Сначала я приведу код активатора, затем позволю себе дать некоторые пояснения.

package org.beq.equinox.service;



import java.util.Properties;



import org.osgi.framework.BundleActivator;

import org.osgi.framework.BundleContext;

import org.osgi.framework.ServiceRegistration;



public class Activator implements BundleActivator {



    public ServiceRegistration registration;

   

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext context) throws Exception {

        // create a service

        IBeqService service = new ColorizerService();

       

        // set some params

        Properties props = new Properties();

        props.put("category", "basic");

       

        // registrating the ColorizerService

        registration = context.registerService(service.getName(), service, props);

    }



    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)

     */


    public void stop(BundleContext context) throws Exception {

        registration.unregister();

    }

}

 


Для регистрации сервиса в реестре используется объект класса ServiceRegistration, представляющий собой запись в реестре о регистрации сервиса. Вся работа непосредственно с реестром, как и любое взаимодействие бандла с OSGi осуществляется через контекст бандла. Метод registerService принимает в качестве параметров имя под которым регистрируется сервис, сам объект сервиса и набор параметров (Properties). Набор параметров может быть пустым, тогда в метод нужно передать null.

В методе stop активатора бандла необходимо освободить все ресурсы, в том числе и отменить регистрацию сервисов. Для этого используется метод unregister класса ServiceRegistration.

Теперь посмотрим на манифест бандла. В нем мы указываем активатор бандла и экспортируемые пэкеджи. В нашем случае - org.beq.equinox.service:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: Service

Bundle-SymbolicName: org.beq.equinox.service

Bundle-Version: 1.0.0

Bundle-Activator: org.beq.equinox.service.Activator

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

Import-Package: org.osgi.framework;version="1.3.0"

Export-Package: org.beq.equinox.service

 


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



Теперь стартуем наш бандл с помощью команды start 1. Выполнится активатор бандла и в реестре сервисов зарегистрируется наш сервис. Посмотреть информацию о зарегистрированных в системе сервисах можно с помощью команды services. Данной команде можно передавать параметры, например имя класса, реализующего данный сервис. Причем в параметрах можно использовать заменители - например знак *. Так посмотреть информацию о нашем сервисе можно с помощью команды: services (objectClass=*ColorizerService):



Как видим сервис успешно зарегистрирован в системе (кстати, можно заметить среди его параметров те, что мы передали при старте), но его никто не использует. И правильно - мы же еще не написали клиента.

Создадим бандл org.beq.equinox.customer, в котором и воспользуемся созданным сервисом. Предположим, что наш клиент строит некое меню выбора цвета, а в качестве источника цветов будет использовать сервис ColorizerService из бандла org.beq.equinox.service. Меню выбора цвета опишем с помощью интерфеса IColorMenu. Код интерфейса следующий:

package org.beq.equinox.customer;



import java.util.List;



public interface IColorMenu

{

    public List<String> getAllItems();

}

 


Как видим - все просто. Есть метод getAllItems, который возвращает список всех доступных цветов. Гораздо интереснее реализации класса-клиента ColorMenu:

package org.beq.equinox.customer;



import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;



import org.beq.equinox.service.ColorizerService;

import org.osgi.util.tracker.ServiceTracker;



public class ColorMenu implements IColorMenu

{

    private final ServiceTracker colorizerTracker;



    public ColorMenu(ServiceTracker tracker)

    {

        colorizerTracker = tracker;

    }

   

    @Override

    public List<String> getAllItems()

    {

        ColorizerService service = (ColorizerService) colorizerTracker.getService();

        if (service == null)

            return new ArrayList<String>();

       

        return Arrays.asList(service.listColors());

    }    

}

 


В данном коде интерес представлют 2 момента. Первый - это некий объект класса ServiceTracker, который передается в конструктор нашего класса. ServiceTracker - это утилитный класс OSGi, который помогает получить сервис, зарегистрированный в системе. Правило здесь простое - для доступа к сервису создается ServiceTracker, после нехитрых манипуляций с которым мы и получим экземпляр сервиса.

Второй момент - сам процесс доступа к сервису, а именно зачем тут проверка на null? Все очень просто - возможно, что наш бандл-клиент будет стартован раньше, чем бандл-предоставляющий сервис, зарегистрирует его в системе. Или бандл, предоставляющий сервис, вообще не установлен. В таком случае на попытку получить сервис, ServiceTracker вернет нам null.

Теперь посмотрим на самое интересное - создание нужного нам экземпляра класса ServiceTracker и работу с клиентом. Все это мы будем делать в активаторе бандла - классе Activator. Код активатора следующий:

package org.beq.equinox.customer;



import java.util.List;



import org.beq.equinox.service.ColorizerService;

import org.osgi.framework.BundleActivator;

import org.osgi.framework.BundleContext;

import org.osgi.util.tracker.ServiceTracker;



public class Activator implements BundleActivator {



    private ServiceTracker tracker;

   

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext context) throws Exception {

        // Create and open tracker

        tracker = new ServiceTracker(context, ColorizerService.class.getName(), null);

        tracker.open();

       

        // Create customer service

        ColorMenu menu = new ColorMenu(tracker);       

       

        // Execute the sample

        List<String> items = menu.getAllItems();

        for (String item : items)

            System.out.println(item);

    }



    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)

     */


    public void stop(BundleContext context) throws Exception {

        // Close the ColorizerService ServiceTracker

        tracker.close();

    }

}

 


При создании экзепляра класса ServiceTracker в его конструктор передаются контекст бандла и имя сервиса, для которого создается трекер. Последним парметром можно передать так называемый ServiceTrackerCustomizer - объект, который может настраивать работу треккера. Если кастомайзер не нужен - передаем null.

После создания треккера его обязательно нужно открыть, иначе через него нельзя будет достучаться до сервиса.

После того как мы открыли треккер можно создать ColorMenu, получить все его элементы и вывести их на консоль.

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

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: Customer

Bundle-SymbolicName: org.beq.equinox.customer

Bundle-Version: 1.0.0

Bundle-Activator: org.beq.equinox.customer.Activator

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

Import-Package: org.beq.equinox.service,

 org.osgi.framework,

 org.osgi.util.tracker

 


Обратите внимание: мы импортируем пэкедж org.osgi.util.tracker.

Посмотрим как это все работает. Запустим Equinox и установим оба бандла - org.beq.equinox.service и org.beq.equinox.customer:



Теперь стартуем бандлы в правильном порядке, т.е. сначала сервис, потом клиент:



Как видим - все работает так, как надо. Клиент подключается к сервису, получает от него информацию и выводит ее на консоль.

Поиздеваемся над нашим клиентом - закомментируем строку tracker.open(); в активаторе:

        //tracker.open();


Заново запустим Equinox, установим бандлы и активируем их в нужной последовательности. Как видим



на экран ничего не вывелось. Потому что закрытый треккер не может вернуть сервис.

Вернем все обратно. Давайте попробуем стартовать бандлы в неправильном порядке - сначала клиентский, а потом сервисный:



После старта клиентского бандла - ничего не вывелось. Потому что клиент не может получить сервис - сервиса в системе нет. После того как мы стартуем бандл с сервисом - необходимо будет выполнить команду refresh, чтобы заново выполнить активатор бандла клиента:



Вот сейчас - все в порядке. Интересно получить информацию по нашему сервису. Напоминаю, что сделать это можно командой services (objectClass=*ColorizerService):



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

А сейчас, подумайте вот над чем: "А что если мы хотим зарегистрировать 2 ColorizerService в разных бандлах, каждый со своей палитрой?" Ведь при описанном способе взаимодействия клиент сможет получить доступ лишь к тому сервису, который будет зарегистрирован последним. О том, как решить данную проблему, мы и поговорим в следующей статье цикла.

Оставайтесь на связи!

Понравилось сообщение - подпишись на блог

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

Alexander Lipatov комментирует...

Что-то я не понял, зачем два интерфейса создавалось? IColorMenu и IBeqService? Как мне показалось, они никакой роли не играют, можно было упростить код, не используя их

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

Читайте следующую заметку, там это объяснено сквозь строк :)

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

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

А есть какой-нибудь стандартный способ передачи объекта между двумя несвязанными плагинами? Т.е. некоторый singleton DataExchenger.

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

Я в эту сторону не копал, поэтому не скажу. Мне пока такой объект не пригождался. Можно задать вопрос на форуме Equinox: http://www.eclipse.org/forums/index.php?t=thread&frm_id=31& Если что-то нароете, пожалуйста дайте знать.

Nikita Eshkeev комментирует...

Вот эта строка уже не работает в новом equinox (Eclipse Luna):
registration = context.registerService(service.getName(), service, props);
метод требует объекта java.util.Dictionary вместо java.util.Properties

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

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