Cервисы взаимодействия бандлов

Модульная система, которая поддерживает только статические зависимости, является хрупкой и не может поддерживать расширяемость. Хрупкость означает, что нельзя удалить какой-либо модуль, не нарушая работоспособности (компиляцию, сборку) приложения в целом. Отсутствие расширяемости означает, что нельзя добавить к системе новый функциональный модуль без перекомпиляции существующей системы.

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

Позднее связывание

OSGi решает проблему «позднего связывания» использованием динамических сервисов, в основе которых лежит принцип внедрения зависимости Dependency injection (DI) — процесс предоставления внешней зависимости программному компоненту. DI является специфичной формой «инверсии управления» (Inversion of control, IoC), когда она применяется к управлению зависимостями. В этом случае объект (bundle) перекладывает ответственность за построение требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму. Следует вспомнить, что в спецификации OSGi оперируют так называемыми бандлами (bundle). В тексте статьи наряду с бандлами можно встретить также и такую сущность, как плагин, значение которого отнесем к синониму.

На следующем рисунке представлена схема взаимодействия бандлов в OSGi контейнере с использованием сервисов.

Предоставляющий определенные услуги bundle должен быть зарегистрирован как сервис (Registr) в реестре контейнера OSGi. Остальные бандлы (client) могут получить на него «ссылку» (Find). После этого необходимо с сервисом связаться (Bind) и использовать его услуги (методы).

Здесь следует обратить внимание на особенности использования данной схемы взаимодействия :

  • bundle-сервис должен быть зарегистрирован раньше bundle-клиент;
  • bundle-клиент должен знать о наличии определенного bundle-сервис в реестре.

Если bundle-сервис независим от других сервисов, то его разработка упрощается : необходимо соответствующим образом определить структуру и методы, после чего инсталировать в контейнер OSGi и зарегистрировать в качестве сервиса. А вот с bundle-клиентом задача немного по-сложнее, поскольку уже при разработке необходимо использовать методы bundle-сервис. Посмотрим, как это будет выглядеть на примере.

Пример использования OSGi сервиса для взаимодействия бандлов

Рассмотрим пример взаимодействия двух бандлов. Один bundle (сервис) будет предоставлять список валют, а другой bundle (клиент) будет подключаться к сервису, получать список валют и выводить его в консоль. При разработке будут созданы два maven-проекта :

  • service-currency : сервис предоставления списка валют;
  • bundle-exchange : клиент использования сервиса.

Описание bundle согласно спецификации OSGi и его отличие от обычного jar-файла подробно описано здесь.

Описание проекта service-currency

Структура сервиса service-currency включает интерфейс ICurrency.java, реализацию CurrencyImpl.java и активатор CurrencyActivator.java.

Листинг интерфейса ICurrency.java

public interface ICurrency 
{
    public String getName();
}

Интерфейс включает один метод getName(), возвращающий наименование сервиса.

Листинг класса CurrencyImpl.java

public class CurrencyImpl implements ICurrency
{
    public String getName() 
    {
        return CurrencyImpl.class.getName();
    }
    public String[] listCurrencies()
    {
        return new String[] {"rub", "usd", "eur"};
    }
}

Класс CurrencyImpl реализует метод getName() интерфейса ICurrency и включает дополнительно метод listCurrencies(), возвращающий список валют в виде массива строк.

Листинг активатора сервиса CurrencyActivator.java

import java.util.Properties;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;

public class CurrencyActivator implements BundleActivator 
{
    ServiceRegistration sr;

    public void start(BundleContext context) throws Exception 
    {
        // создание класса реализации сервиса
        ICurrency service = new CurrencyImpl();

    	// определение свойств сервиса
        Properties props = new Properties();
        props.put("service", "currency");

        // регистрация сервиса
        sr = context.registerService(service.getName(), service, props);
        System.out.println("start service <currency>");
    }
	
    public void stop(BundleContext context) throws Exception
    {
        sr.unregister();
        System.out.println("stop service <currency>");
    }
}

Активатор сервиса при старте (метод start) получает в качестве параметра контекст контейнера context типа BundleContext, в котором регистрирует сервис вызовом метода контекста registerService. Метод registerService возвращает объект типа ServiceRegistration, который используется при останове сервиса в методе stop. В качестве параметров для регистрации сервиса в метод registerService передаются наименование сервиса (класса), экземляр сервиса и свойства. Свойства сервиса можно не определять, тогда в метод нужно передать значение null.

В методе stop освобождаются ресурсы сервиса (bundle) и отменяется регистрация сервиса вызовом метод unregister класса ServiceRegistration.

Описание сборки pom.xml

Для сборки проекта service-currency используется pom.xml следующего содержания :

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                    http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.osgi.service</groupId>
  <artifactId>service-currency</artifactId>
  <packaging>bundle</packaging>
  <version>1.0.0</version>

  <name>Service currency bundle</name>
  <url>http://maven.apache.org</url>

  <properties>
      <project.build.sourceEncoding>
          UTF-8
      </project.build.sourceEncoding>
  </properties>
  
  <dependencies>
      <dependency>
          <groupId>org.apache.felix</groupId>
          <artifactId>org.osgi.core</artifactId>
          <version>1.4.0</version>
      </dependency>
  </dependencies>
  
   <build>
      <plugins>
          <plugin>
              <groupId>org.apache.felix</groupId>
              <artifactId>maven-bundle-plugin</artifactId>
              <version>2.3.7</version>
              <extensions>true</extensions>
              <configuration>
                  <instructions>
                      <Bundle-SymbolicName>
                             ${project.groupId}.${project.artifactId}
                      </Bundle-SymbolicName>
                      <Bundle-Name>
                             ${project.name}
                      </Bundle-Name>
                      <Bundle-Version>
                             ${project.version}
                      </Bundle-Version>
                      <Bundle-Localization>
                             plugin
                      </Bundle-Localization>
                      <Bundle-Activator>
                             ${project.groupId}.CurrencyActivator
                      </Bundle-Activator>
                      <Import-Package>
                             org.osgi.framework;version="1.4.0"
                      </Import-Package>
                      <Export-Package>
                             ${project.groupId}
                      </Export-Package>
                  </instructions>
              </configuration>
          </plugin>
      </plugins>
  </build>
</project>

В файле описания сборки pom.xml определяется зависимость фреймворка osgi (секция <dependency>) и параметры bundle для формирования манифеста META-INF/MANIFEST.MF. В результате сборки в поддиректории проекта target будет создан bundle в виде файла service-currency-1.0.0.jar с манифестом следующего содержания :

Manifest-Version: 1.0
Bnd-LastModified: 1487417732151
Build-Jdk: 1.7.0_67
Bundle-Activator: com.osgi.service.CurrencyActivator
Bundle-Localization: plugin
Bundle-ManifestVersion: 2
Bundle-Name: Service currency bundle
Bundle-SymbolicName: com.osgi.service.service-currency
Bundle-Version: 1.0.0
Created-By: Apache Maven Bundle Plugin
Export-Package: com.osgi.service;uses:="org.osgi.framework";version="1.0.0"
Import-Package: org.osgi.framework;version="1.4.0"
Tool: Bnd-1.50.0

Инсталляция и регистрация сервиса

Для тестирования созданного сервиса используем включенный в архив примера папку <osgi.container>, в которой размещается org.eclipse.osgi_ХХХ.jar для создания контейнера Equinox. О командах работы в контейнере можно прочитать здесь. Запустим контейнер Equinox, инсталлируем в контейнер bundle-сервис и стартуем его :


java -jar org.eclipse.osgi_3.7.1.R37x_v20110808-1106.jar -console

osgi> ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
  
osgi> install file:/d:/osgi.container/bundles/service-currency-1.0.0.jar
Bundle id is 1

osgi> ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1       INSTALLED   com.osgi.service.service-currency_1.0.0

osgi> start 1
start service <currency>

osgi> ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
3       ACTIVE      com.osgi.service.service-currency_1.0.0

osgi> services (objectClass=com.osgi.service.CurrencyImpl)
{com.osgi.service.CurrencyImpl}={service=currency, service.id=29}
  Registered by bundle: com.osgi.service.service-currency_1.0.0 [1]
  No bundles using service.
  
osgi> stop 1
stop service <currency>

osgi> ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
3       RESOLVED    com.osgi.service.service-currency_1.0.0  
 

В процессе тестирования bundle-сервиса мы проводим его через все стадии жизненного цикла выполнением команд install, start, stop. Командой services с соответствующим параметром в консоль была выведена информация о сервисе :

  • используемый в качестве сервиса класс com.osgi.service.CurrencyImpl;
  • свойство props (service=currency), определенное при регистрации;
  • идентификационный номер сервиса, в нашем случае service.id=29;
  • кем зарегистрирован сервис;
  • отсутствие подключенных бандлов.

Примечание : для просмотра конкретного сервиса можно использовать маску в виде символа *, чтобы не писать полный путь (пакет, наименование), т.е. наш сервис можно было бы вывести в консоль командой osgi> services (objectClass=*CurrencyImpl).

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

Таким образом, можно заключить, что сервис успешно загружен и зарегистрирован в системе, но никто к нему не подключен.

Описание проекта bundle-exchange

Структура плагина bundle-exchange включает интерфейс IExchange.java, реализацию ExchangeImpl.java и активатор ExchangeActivator.java.

Листинг интерфейса IExchange.java

import java.util.List;

public interface IExchange 
{
    public List<String> getCurrencies();
}

Интерфейс включает один метод getCurrencies(), возвращающий список текстовых значений.

Листинг класса ExchangeImpl.java

import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;

import org.osgi.util.tracker.ServiceTracker;

import com.osgi.service.CurrencyImpl;

public class ExchangeImpl implements IExchange
{
    private final ServiceTracker<Object, Object> serviceTracker;

    public ExchangeImpl (ServiceTracker<Object, Object> tracker)
    {
        serviceTracker = tracker;
    }

    public List<String> getCurrencies()
    {
        CurrencyImpl service = (CurrencyImpl)serviceTracker.getService();
        if (service == null) {
            System.out.println ("service is null");
            return new ArrayList<String>();
        }
        return Arrays.asList(service.listCurrencies());
    }
}

При доступе к сервису в методе getCurrencies() выполняется проверка на null. Если сервис не найден, то метод вернет пустой список. Это может быть связано с тем, что сервис может отсутствовать или не быть зарегистрированным в системе. В этом случае на попытку получить сервис, ServiceTracker вернет нам null.

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

C помощью ServiceTracker можно :
  - создать начальный список указанных сервисов;
  - отслеживать события Service`Event этих сервисов;
  - позволять владельцу списка программно его изменять, равно как производить какие-либо действия в момент добавления или удаления сервиса в список.

Конструкторы объекта ServiceTracker

ServiceTracker(BundleContext, ServiceName    , ServiceTrackerCustomizer);
ServiceTracker(BundleContext, Filter         , ServiceTrackerCustomizer);
ServiceTracker(BundleContext,ServiceReference, ServiceTrackerCustomizer);

Каждый из конструкторов принимает в качестве параметра BundleContext. Первый конструктор используется для получения имени интерфейса сервиса в качестве критерия поиска. Второй конструктор используется для отслеживания всех сервисов, подпадающих под указанный фильтр. Третий конструктор принимает в качестве аргумента ServiceReference, который позволяет отслеживать конкретный сервис.

Как только ServiceTracker создан, он начинает отслеживать сервисы и позволяет использовать следующие свои методы :

  • getService()/getServices() - возвращают отслеживаемые трэкером сервис/ы;
  • getServiceReference()/getServiceReferences() - позволяют вызвавшему метод находится в ожидании до тех пор, пока не появится хотя бы одна инстанция отслеживаемого сервиса, либо до истечения таймаута.

Примечание : не рекомендуется использовать getServiceReference()/getServiceReferences() в активаторах бандла, так как предполагается, что активаторы бандла должны быстро загружаться, а фреймворк может приостановить выполнение активатора до загрузки бандла, загружающего нужный сервис, что может привести к deadlock-у, если сервис не будет загружен.

Листинг активатора ExchangeActivator.java

import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleActivator;
import org.osgi.util.tracker.ServiceTracker;

import com.osgi.service.CurrencyImpl;

public class ExchangeActivator implements BundleActivator
{
    private ServiceTracker<Object, Object> tracker;

    public void start(BundleContext context) throws Exception
    {
        System.out.println("start bundle exchange");

        // создание и открытие трекера
        tracker = new ServiceTracker<Object, Object>(
                           context, CurrencyImpl.class.getName(), null);
        tracker.open();

        ExchangeImpl list = new ExchangeImpl(tracker);        

        // вывод в консоль списка валют
        System.out.println("currencies list");
        for (String item : list.getCurrencies())
            System.out.println(item);
    }

    public void stop(BundleContext context) throws Exception
    {
        System.out.println("stop bundle exchange");
        tracker.close();
    }
}

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

Описание сборки pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.osgi.bundle</groupId>
  <artifactId>bundle-exchange</artifactId>
  <packaging>bundle</packaging>
  <version>1.0.0</version>

  <name>Bundle exchange Plug-in</name>
  <url>http://maven.apache.org</url>

  <properties>
      <project.build.sourceEncoding>
          UTF-8
      </project.build.sourceEncoding>
  </properties>
  
  <dependencies>
      <dependency>
          <groupId>org.apache.felix</groupId>
          <artifactId>org.osgi.core</artifactId>
          <version>1.4.0</version>
      </dependency>
      <dependency>
          <groupId>org.osgi</groupId>
          <artifactId>org.osgi.util.tracker</artifactId>
          <version>1.5.1</version>
      </dependency>
      <dependency>
          <groupId>com.osgi.service</groupId>
          <artifactId>service-currency</artifactId>
          <version>1.0.0</version>
          <scope>provided</scope>
      </dependency>
  </dependencies>
  
   <build>
      <plugins>
          <plugin>
              <groupId>org.apache.felix</groupId>
              <artifactId>maven-bundle-plugin</artifactId>
              <version>2.3.7</version>
              <extensions>true</extensions>
              <configuration>
                  <instructions>
                     <Bundle-SymbolicName>
                             ${project.groupId}.${project.artifactId}
                     </Bundle-SymbolicName>
                     <Bundle-Name>
                             ${project.name}
                     </Bundle-Name>
                     <Bundle-Version>
                             ${project.version}
                     </Bundle-Version>
                     <Bundle-Activator>
                            ${project.groupId}.ExchangeActivator
                     </Bundle-Activator>
                     <Private-Package>
                           ${project.groupId}
                     </Private-Package>
                  </instructions>
              </configuration>
          </plugin>
      </plugins>
   </build>
</project>

В файле описания сборки pom.xml определяется зависимость плагина от артифакта service-currency на время разработки (provided), необходимая для компиляции и создания bundle. Чтобы maven нашел данный артифакт в репозитории необходимо выполнить в предыдущем проекте service-currency команду mvn install, которая загрузит артифакт в репозиторий.

В результате сборки в поддиректории проекта target будет создан bundle в виде файла bundle-exchange-1.0.0.jar с манифестом следующего содержания :

Manifest-Version: 1.0
Bnd-LastModified: 1487435015998
Build-Jdk: 1.7.0_67
Bundle-Activator: com.osgi.bundle.ExchangeActivator
Bundle-ManifestVersion: 2
Bundle-Name: Bundle exchange Plug-in
Bundle-SymbolicName: com.osgi.bundle.bundle-exchange
Bundle-Version: 1.0.0
Created-By: Apache Maven Bundle Plugin
Export-Package: com.osgi.bundle;uses:="com.osgi.service,
             org.osgi.util.tracker,org.osgi.framework";version="1.0.0"
Import-Package: com.osgi.service;version="[1.0,2)",org.osgi.framework;
              version="[1.5,2)",org.osgi.util.tracker;version="[1.5,2)"
Tool: Bnd-1.50.0

Тестирование

Как было отмечено выше, в архив примеров входит папка «osgi.container», в которой размещается org.eclipse.osgi_ХХХ.jar для создания контейнера Equinox. Для автоматической загрузки плагинов (бандлов) их необходимо разместить в поддиректории «osgi.container/bundles», а в поддиректории «osgi.container/configuration» настроить файл инициализации config.ini. После этого бандлы будут автоматически загружены в контейнер, сервис будет зарегистрирован и к нему подключится клиент. А в консоли мы увидим следующую «картину» :


java -jar org.eclipse.osgi_3.7.1.R37x_v20110808-1106.jar -console

osgi> start service <currency>
start bundle exchange
currencies list
rub
usd
eur
ss

Framework is launched.

id      State       Bundle
0       ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1       ACTIVE      com.osgi.service.service-currency_1.0.0
2       ACTIVE      com.osgi.bundle.bundle-exchange_1.0.0

osgi> services (objectClass=com.osgi.service.CurrencyImpl)
{com.osgi.service.CurrencyImpl}={service=currency, service.id=29}
  Registered by bundle: com.osgi.service.service-currency_1.0.0 [1]
  Bundles using service:
    com.osgi.bundle.bundle-exchange_1.0.0 [2]
 

Можно сказать поставленная задача решена и бандлы «нашли друг друга» на просторах контейнера Equinox.

Скачать исходный код

Исходные коды рассмотренного примера взаимодействия bundle для платформы OSGi в виде проектов Eclipse можно скачать здесь (1.2 Мб). В архив проекта дополнительно включена директория osgi.container для создания контейнера Equinox и тестирования взаимодействия бандлов.

Пример реализации механизма publish/subscribe (публикации/подписки) в OSGi приложения с использованием фреймворка Felix представлен здесь.

  Рейтинг@Mail.ru