воскресенье, 22 марта 2009 г.

Введение в OSGi. Динамический ServiceTracker. Две стратегии использования сервисов.


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

Еще раз вернемся к условию нашей задачи. Нам необходимо разработать меню цветов, котороее формировалось бы из палитр, предоставляемых разными бандлами. Мы выбрали такую схему решения: бандлы выставляют ColorizerService'ы, предоставляющие палитры меню. И есть некий бандл, который мы будем называть "центральным", который получает эти палитры от сервисов и объединяет их.

Приступим к реализации? Сначала создадим бандлы с сервисами. Пусть у нас будет два бандла: org.beq.equinox.p1 и org.beq.equinox.p2. Код и манифесты у них будут одинаковыми, отличаться будут лишь массивы цветов и конечно же значения полей Bundle-Name и Bundle-SymbolicName в манифестах. Поэтому сосредоточимся только на одном бандле org.beq.equinox.p1.


Код бандла будет состоять из класса-сервиса ColorizerService и класса-активатора Activator. Код сервиса следующий:

  1. package org.beq.equinox.p1;

  2.  

  3. import org.beq.equinox.colorizer.IColorizer;

  4.  

  5. public class ColorizerService implements IColorizer

  6. {

  7.     @Override

  8.     public String[] getColors()

  9.     {

  10.         return new String[] {

  11.                 "Red", "Green", "Blue"

  12.         };

  13.     }

  14. }

  15.  



Собственно код тривиален. Скажу лишь, что интерфейс IColorizer предоставляется нам "центральным" бандлом.

Активатор бандла org.beq.equinox.p1:

  1. package org.beq.equinox.p1;

  2.  

  3. import org.beq.equinox.colorizer.IColorizer;

  4. import org.osgi.framework.BundleActivator;

  5. import org.osgi.framework.BundleContext;

  6. import org.osgi.framework.ServiceRegistration;

  7.  

  8. public class Activator implements BundleActivator {

  9.    

  10.     private ServiceRegistration registration;

  11.    

  12.     /*

  13.      * (non-Javadoc)

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

  15.      */

  16.     public void start(BundleContext context) throws Exception {

  17.         // create a service

  18.         IColorizer colorizer = new ColorizerService();

  19.        

  20.         // registrating the ColorizerService

  21.         registration = context.registerService(IColorizer.class.getName(),

  22.                 colorizer, null);

  23.     }

  24.  

  25.     /*

  26.      * (non-Javadoc)

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

  28.      */

  29.     public void stop(BundleContext context) throws Exception {

  30.         registration.unregister();

  31.     }

  32. }

  33.  



Комментировать здесь нечего. Реализован стандартный сценарий регистрации сервиса.

Думаю разумно будет так-же привести манифест бандла:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: P1

Bundle-SymbolicName: org.beq.equinox.p1

Bundle-Version: 1.0.0

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

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

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

 org.osgi.framework

 


Самое интересное - "центральный" бандл org.beq.equinox.colormenu, который будет объединять данные, предоставляемые сервисами ColorizerService. Основным классом бандла является класс ColorMenu, который реализует интерфейс IColorMenu. Основная идея - в классе определена коллекция для хранения палитр и методы добавления палитры в коллекцию и удаления палитры из коллекции.

IColorMenu:
package org.beq.equinox.colormenu;



import java.util.List;



public interface IColorMenu

{

    public List<String> getAllItems();    

}

 


ColorMenu:
package org.beq.equinox.colormenu;



import java.util.ArrayList;

import java.util.Collection;

import java.util.Collections;

import java.util.List;



import org.beq.equinox.colorizer.IColorizer;



public class ColorMenu implements IColorMenu

{

    private Collection<IColorizer> colorizers

            = Collections.synchronizedCollection(new ArrayList<IColorizer>());

   

    @Override

    public List<String> getAllItems()

    {

        List<String> results = new ArrayList<String>();

        for (IColorizer colorizer : colorizers)

        {

            for (String color: colorizer.getColors())

                if (!results.contains(color))

                    results.add(color);

        }

       

        return results;

    }



    protected synchronized void bindColorizer(IColorizer colorizer)

    {

        colorizers.add(colorizer);

        System.out.println("- bind new colorizer");

    }

   

    protected synchronized void unbindColorizer(IColorizer colorizer)

    {

        colorizers.remove(colorizer);

        System.out.println("- unbind colorizer");

    }    

}

 


Самый главный вопрос - а кто будет добавлять палитры в меню? Ведь перед тем, как добавить палитры в меню их необходимо получить от сервисов. В предыдущей заметке мы рассматривали работу с сервисами и выяснили, что существует класс ServiceTracker, который отслеживает регистрацию сервиса с заданным именем в реестре сервисов OSGi и позволяет получить доступ к зарегистрированному экземпляру сервиса. Получается, чтобы получить доступ к данным, предоставляемым всеми зарегистрированными в реестре сервисам необходимо повесить свой хук на процедуру регистрации сервиса. Роль этого хука будет играть класс ColorizerTracker, являющийся наследником ServiceTracker:

package org.beq.equinox.colormenu;



import org.beq.equinox.colorizer.IColorizer;

import org.osgi.framework.BundleContext;

import org.osgi.framework.ServiceReference;

import org.osgi.util.tracker.ServiceTracker;

import org.osgi.util.tracker.ServiceTrackerCustomizer;



public class ColorizerTracker extends ServiceTracker implements ServiceTrackerCustomizer

{  

    private final ColorMenu menu = new ColorMenu();

   

    public ColorMenu getMenu()

    {

        return menu;

    }

   

    public ColorizerTracker(BundleContext context)

    {

        super(context, IColorizer.class.getName(), null);

        open();

    }



    @Override

    public Object addingService(ServiceReference reference) {

       

        IColorizer colorizer = (IColorizer) context.getService(reference);

        menu.bindColorizer(colorizer);

       

        return colorizer;

    }

   

    @Override

    public void removedService(ServiceReference reference, Object service)

    {

        menu.unbindColorizer((IColorizer) service);

        super.removedService(reference, service);

    }

}

 


Интерфейс ServiceTrackerCustomizer определяет методы, отслеживающие изменение состояний сервисов. Мы реализовали 2 метода: public Object addingService(ServiceReference reference) и public void removedService(ServiceReference reference, Object service). Соответственно, при регистрации сервиса в реестре мы получим предоставляемую им палитру и сохраним ее в коллекции colorizers.

Интерфейс IColorizer, используемый в этом и сервисных бандлах:

package org.beq.equinox.colorizer;



public interface IColorizer

{

    public String[] getColors();

}

 


В активаторе достаточно создать объект класса ColorizerTracker для того, чтобы бандл начал реагировать на регистрацию сервисов в реестре. Код активатора:

package org.beq.equinox.colormenu;



import org.osgi.framework.BundleActivator;

import org.osgi.framework.BundleContext;



public class Activator implements BundleActivator {



    private ColorizerTracker tracker;

   

    /*

     * (non-Javadoc)

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

     */


    public void start(BundleContext context) throws Exception {

        tracker = new ColorizerTracker(context);

       

        printColors();

    }



    /*

     * (non-Javadoc)

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

     */


    public void stop(BundleContext context) throws Exception {

        printColors();

       

        tracker.close();

    }

   

    private void printColors()

    {

        IColorMenu menu = tracker.getMenu();      

        for (String item : menu.getAllItems())

            System.out.println(item);

    }

}

 


Также стоит рассмотреть манифест бандла:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: ColorMenu

Bundle-SymbolicName: org.beq.equinox.colormenu

Bundle-Version: 1.0.0

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

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

Import-Package: org.osgi.framework,

 org.osgi.util.tracker

Export-Package: org.beq.equinox.colorizer

 


Здесь следует обратить внимание на то, что мы экспортируем пэкедж org.beq.equinox.colorizer, в котором находится интерфейс IColorizer.

Темерь можно посмотреть на то, как все это работает. Стартуем Equinox и инсталлируем наши бандлы на шину. Порядок установки и id бандлов приведены на рисунке.



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



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



Единственным интерфейсом к бандлу для нас пока является консоль. Остановим клиентский бандл. Так как в методе stop мы сначала выводим на экран известные бандлу цвета, то при попытке остановить бандл увидим следующее:



Т.е. после того, как мы зарегистрировали сервисы в реестре их данные стали доступны клиентскому бандлу.

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

Применительно к нашей задаче суть идеи следующая: мы регистрируем сервис ColorMenuService в реестре сервисов. Другие бандлы не будут получать от него данные, а наоборот - будут предоставлять их ему. Ключевым звеном в данной связке будет являться MenuTracker, через который осуществляется биндинг данных в сервис.

Собственно код будет основан на коде предыдущего примера. Мы создадим бандл org.beq.equinox.colormenu2, в котором определим сервис и бандлы org.beq.equinox.p1c и org.beq.equinox.p2c, которые будут предоставлять данные нашему сервису.

Начнем с бандла org.beq.equinox.colormenu2. Код сервиса ColorMenuService будет полностью повторять код класса ColorMenu. Экспортируемый бандлом IColorizer тоже остается без изменений. А вот вместо ColorizerTracker мы создадим класс MenuTracker, код которого будет вот таким:

package org.beq.equinox.colormenu;



import java.util.ArrayList;

import java.util.List;



import org.beq.equinox.colorizer.IColorizer;

import org.osgi.framework.BundleContext;

import org.osgi.framework.ServiceReference;

import org.osgi.util.tracker.ServiceTracker;

import org.osgi.util.tracker.ServiceTrackerCustomizer;



public class MenuTracker extends ServiceTracker implements ServiceTrackerCustomizer

{  

    private List<IColorizer> colorizers = new ArrayList<IColorizer>();

   

    private List<IColorizer> colorizers2connect = new ArrayList<IColorizer>();

   

    public MenuTracker(BundleContext context)

    {

        super(context, ColorMenuService.class.getName(), null);

        open();

        connectColorizers((ColorMenuService) getService());

    }

   

    public MenuTracker(BundleContext context, List<IColorizer> colorizers)

    {

        super(context, ColorMenuService.class.getName(), null);

        if (colorizers != null)

            colorizers2connect.addAll(colorizers);

        open();

        connectColorizers((ColorMenuService) getService());

    }

   

    public MenuTracker(BundleContext context, IColorizer colorizer)

    {

        super(context, ColorMenuService.class.getName(), null);

        if (colorizers != null)

            colorizers2connect.add(colorizer);

        open();

        connectColorizers((ColorMenuService) getService());

    }

   

    @Override

    public Object addingService(ServiceReference reference) {

        Object service = super.addingService(reference);

        connectColorizers((ColorMenuService) service);

        return service;

    }

   

    @Override

    public void removedService(ServiceReference reference, Object service)

    {

        colorizers2connect.addAll(colorizers);

        colorizers.clear();

        super.removedService(reference, service);

    }

   

    protected void connectColorizers(ColorMenuService service)

    {

        if (service != null)

        {

            for (IColorizer colorizer: colorizers2connect)

                connectColorizer(colorizer, service);

           

            colorizers2connect.clear();

        }        

    }

   

   

    public void connectColorizer(IColorizer colorizer, ColorMenuService service)

    {

        if (colorizer != null && service != null)

        {

            service.bindColorizer(colorizer);

            colorizers.add(colorizer);

        }

    }

   

    protected void disconnectColorizers(ColorMenuService service)

    {

        if (service != null)

        {

            for (IColorizer colorizer: colorizers)

                disconnectColorizer(colorizer, service);

            colorizers.clear();

        }

    }

   

    public void disconnectColorizer(IColorizer colorizer, ColorMenuService service)

    {

        if (colorizer != null && service != null)        

            service.unbindColorizer(colorizer);

    }

   

    @Override

    public synchronized void close()

    {

        disconnectColorizers((ColorMenuService) getService());

        super.close();

    }

}

 


Как видим - класс стал несколько сложнее. У нас теперь 2 коллекции - в одну заносятся переданные в сервис палитры, в другую - доступные для передачи. Основная идея в том, что мы создаем треккер, заполняем коллекцию доступных для передачи в сервис объектов, а как только нужный нам сервис зарегистрируется в системе - мы сразу же инъектируем в него данные. Соответственное, если сервис извлекают из реестра данные становятся не "переданными", а "готовыми к передаче" в сервис. Т.е. как только сервис вновь зарегистрируется в реестре - мы с радостью инъектируем в него данные заново.

Так же мы переопределили метод close. Перед тем, как закрыть треккер необходимо отбиндить данные, переданные в сервис.

Активатор банд;;ла тривиальный. Мы создаем экземпляр класса ColorMenuService и регистрируем его в реестре сервисов.

Манифест бандла следующий:

Manifest-Version: 1.0

Bundle-ManifestVersion: 2

Bundle-Name: ColorMenu

Bundle-SymbolicName: org.beq.equinox.colormenu2

Bundle-Version: 2.0.0

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

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

Import-Package: org.osgi.framework,

 org.osgi.util.tracker

Export-Package: org.beq.equinox.colorizer, org.beq.equinox.colormenu

 


Здесь стоит обратить внимание на то, что мы экспортируем 2 пэкеджа: org.beq.equinox.colorizer, содержащий интерфейс IColorizer и org.beq.equinox.colormenu, содержащий наш MenuTracker.

Бандлы org.beq.equinox.p1c и org.beq.equinox.p2c так же не сильно отличаются от org.beq.equinox.p1 и org.beq.equinox.p2 соответственно. Класcы ColorizerService переименованы в Colorizer, а активаторы приняли следующий вид:

package org.beq.equinox.p1c;



import org.beq.equinox.colormenu.MenuTracker;

import org.osgi.framework.BundleActivator;

import org.osgi.framework.BundleContext;



public class Activator implements BundleActivator {



    private MenuTracker tracker;

   

    /*

     * (non-Javadoc)

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

     */


    public void start(BundleContext context) throws Exception {

        tracker = new MenuTracker(context, new Colorizer());

    }



    /*

     * (non-Javadoc)

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

     */


    public void stop(BundleContext context) throws Exception {

        tracker.close();

    }

}

 


Т.е. подключение к сервису выливается в создание экземпляра класса MenuTracker, которому передается нужный экземпляр класса Colorizer.

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

Считаю, что гораздо важнее обсудить эти 2 стратегии. Мое мнение таково: вторая стратегия (т.е. стратегия "1 сервис <-> много клиентов) предпочтительнее. Выгод от ее использования я вижу несколько.

Во-первых - концептуальная. Мы можем рассматривать сервис как точку расширения приложения, а треккеры - как механизм использования этой точки расширения. Мы расширяем возможности нашего приложения, передавая в точку расширения некоторую информацию. В данном случае - Colorizer'ы.

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

Именно такой подход реализован в Naumen Kernel, где используется следующая терминология:

  • Ресурс - POJO, которое инкапсулирует в себя атомарную порцию данных, передаваемых сервису.

  • Сервис - OSGi-сервис, любой ява-класс, который реализует соответствующий интерфейс и позволяет подключать к себе ресурсы.

  • Коннектор - класс, наследующий ServiceTracker. Соответственно бандл, желающий расширить сервис должен создать соответствующий коннектор в своем активаторе.


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

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

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

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

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

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

На Брайтоне тоже ничего, весна!!

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

И это не может не радовать )))

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

Хотелось бы поинтересоваться еще на счет одной интересной для меня темы, а именно Rich Ajax Platform (RAP).. Планируете ли Вы рассказать что нибудь про это?. Материала по данной теме крайне мало, а тем более на русском языке((

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

Сам я RAP не использовал и в ближайшее время не планирую, поэтому врятли смогу рассказать что-либо интересное.

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

Эх, жаль.. Так с трудом дается(

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

Будем ждать новых интересных статей)

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

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

Там еще вроде бы есть сервис листенеры. Которые позволяют зарегаться и ждать, когда же сервис появится. Это я видел в вебинаре, в одном из перечисленных в первой статье.

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

Есть и такое, но суть в том, что кто-то должен стартовать бандл, в котором регистрируется сервис. В этом то и есть вся загвоздка.

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

Добрый день.

Увидел, что вы по долгу службы используете (или разрабатываете naumen kernel). Крайне любопытно узнать, но заверениям производителя это open-source ядро, однако по известным ссылка, выйти на cvs не получилось (возможно плохо искал), а по той ссылке, что приводите вы, лежит архив с версией двух летней давности.
В связи с эти вопрос, ядро загнулось или просто изменился курс с open-source на другой, или ядро как бы не изменяется, а дописываются только уже закрытые плагины?
Вобщем любопытно было бы знать, как обстоят с ним дела, можно ли его поиспользовать для создания реальных проектов, и где взять свежие исходники?

Спасибо.
Николай.

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

Здравствуйте.

По поводу Naumen Kernel вопрос сложный, политику его лицензирования по-моему не знает никто. Ядро развивается, но новые сборки не выкладываются в публичный доступ. К SVN также доступ ограничен только офисами компании + пароли/логины разработчиков. Получается некий отход от опенсорц, впрочем компания имеет на это право - весь код принадлежит ей. Однако, то ядро, что выложено в публичный доступ вы имеете право использовать. Другое дело есть ли смысл ведь получать апдейты и исправления ошибок вы не сможете.

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

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