Hatena::ブログ(Diary)

Viz/Sci/Dev Log RSSフィード Twitter

2008-07-08 OSGiとSpring Frameworkの融合 3

実際にEclipseでSpring Dynamic Modulesを利用したOSGiサービスとコンシューマを作ってみる

全体の設定とEclipseヘのインポート

まずテンプレ作成のスクリプトを実行。更に短く、かつ最新のバンドルを利用するように修正しました。

#!/bin/sh

# Create Project "hello"
pax-create-project -g org.cytoscape -a hello
cd hello

# Add SpringSource Enterprise Bundle Repositories
pax-add-repository -i com.springsource.repository.bundles.release -u http://repository.springsource.com/maven/bundles/release
pax-add-repository -i com.springsource.repository.bundles.external -u http://repository.springsource.com/maven/bundles/external

# Import required bundles
pax-import-bundle -g org.springframework.osgi -a org.springframework.osgi.extender -v 1.1.0 -- -DimportTransitive -DwidenScope

# Create bundles
pax-create-bundle -p org.cytoscape.helloservice -- -Dspring=2.5.5 -Djunit
pax-create-bundle -p org.cytoscape.helloserviceuser -- -Dspring=2.5.5 -Djunit

# Build and run the project
mvn clean install pax:provision

実行されているバンドルpsコマンドで確認する

-> ps
START LEVEL 6
   ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.4)
[   1] [Active     ] [    5] Spring DM Extender (1.1.0)
[   2] [Active     ] [    5] Apache Commons Logging (1.1.1)
[   3] [Active     ] [    5] Spring Beans (2.5.5.A)
[   4] [Active     ] [    5] Spring Context (2.5.5.A)
[   5] [Active     ] [    5] Spring Core (2.5.5.A)
[   6] [Active     ] [    5] Spring DM Core (1.1.0)
[   7] [Active     ] [    5] AOP Appliance API (1.0.0)
[   8] [Active     ] [    5] Spring AOP (2.5.5.A)
[   9] [Active     ] [    5] Spring DM IO (1.1.0)
[  10] [Active     ] [    5] org.cytoscape.helloservice (1.0.0.SNAPSHOT)
[  11] [Active     ] [    5] org.cytoscape.helloserviceuser (1.0.0.SNAPSHOT)
[  12] [Active     ] [    1] osgi.compendium (4.0.1)
[  13] [Active     ] [    1] Apache Felix Shell Service (1.0.1)
[  14] [Active     ] [    1] Apache Felix Shell TUI (1.0.1)
->

Spring DM Extenderが、"Spring-Powered"なバンドルを認識しているか確認する

System Bundle (0) provides:
---------------------------
org.osgi.service.startlevel.StartLevel
org.osgi.service.packageadmin.PackageAdmin

Spring DM Extender (1) provides:
--------------------------------
org.springframework.beans.factory.xml.NamespaceHandlerResolver
org.xml.sax.EntityResolver

org.cytoscape.helloservice (10) provides:
-----------------------------------------
org.springframework.osgi.context.DelegatedExecutionOsgiBundleApplicationContext, org.springframework.osgi.context.ConfigurableOsgiBundleApplicationContext, org.springframework.context.ConfigurableApplicationContext, org.springframework.context.ApplicationContext, org.springframework.context.Lifecycle, org.springframework.beans.factory.ListableBeanFactory, org.springframework.beans.factory.HierarchicalBeanFactory, org.springframework.context.MessageSource, org.springframework.context.ApplicationEventPublisher, org.springframework.core.io.support.ResourcePatternResolver, org.springframework.beans.factory.BeanFactory, org.springframework.core.io.ResourceLoader, org.springframework.beans.factory.DisposableBean

org.cytoscape.helloserviceuser (11) provides:
---------------------------------------------
org.springframework.osgi.context.DelegatedExecutionOsgiBundleApplicationContext, org.springframework.osgi.context.ConfigurableOsgiBundleApplicationContext, org.springframework.context.ConfigurableApplicationContext, org.springframework.context.ApplicationContext, org.springframework.context.Lifecycle, org.springframework.beans.factory.ListableBeanFactory, org.springframework.beans.factory.HierarchicalBeanFactory, org.springframework.context.MessageSource, org.springframework.context.ApplicationEventPublisher, org.springframework.core.io.support.ResourcePatternResolver, org.springframework.beans.factory.BeanFactory, org.springframework.core.io.ResourceLoader, org.springframework.beans.factory.DisposableBean

Apache Felix Shell Service (13) provides:
-----------------------------------------
org.apache.felix.shell.ShellService, org.ungoverned.osgi.service.shell.ShellService
->

大丈夫そうなので、shutdownにて一旦終了します。helloディレクトリに移動。ディレクトリ内はこんな感じです:

drwxr-xr-x    7 kono users 4.0K  7月  8 17:32 .
drwxr-xr-x  112 kono users 4.0K  7月  8 17:32 ..
drwxr-xr-x    4 kono users 4.0K  7月  8 17:32 org.cytoscape.helloservice
drwxr-xr-x    4 kono users 4.0K  7月  8 17:32 org.cytoscape.helloserviceuser
-rw-r--r--    1 kono users 2.6K  7月  8 17:32 pom.xml
drwxr-xr-x    4 kono users 4.0K  7月  8 17:32 poms
drwxr-xr-x    2 kono users 4.0K  7月  8 17:32 provision
drwxr-xr-x    4 kono users 4.0K  7月  8 17:32 runner

ではEclipseのプロジェクトを作成しましょう。

mvn pax:eclipse

そしてそれをEclipseインポートします。予め、Speing IDEとm2eclipseプラグインインストールしておいてください。Mavenプロジェクトとしてインポートします。helloserviceとhelloserviceuserだけインポートすれば大丈夫です:

f:id:keiono:20080709094609p:image

インポート後のプロジェクトはこんな感じになります。プロジェクト名を右クリックして、Springプロジェクトとしての属性を設定してください。

f:id:keiono:20080709044115p:image

Springの設定ファイルはここにあります:

f:id:keiono:20080709095147p:image

OSGiサービスの作成

以下は前提条件として、Springの基礎の基礎を知っていると仮定して話を進めます。

ではまずサービスから作りましょう。とは言っても、もうすでにサンプルのbeanが自動生成されていますので、それを元に作ることにします。ExampleBeanというインターフェースの方に、メソッドを一つ加えましょう:

public interface ExampleBean {
	public boolean isABean();
	public void sayHello();
}

そしてそれを実装:

public class ExampleBeanImpl implements ExampleBean {
	public boolean isABean() {
		return true;
	}
	
	public void sayHello() {
		System.out.println("Hello, OSGi World!");
	}
}

次は、このBeanをOSGiサービスとしてエクスポートしてみましょう。もうすでにBeanとしての定義は自動生成されています:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

	<!-- regular spring configuration file defining the beans for this
		bundle. We've kept the osgi definitions in a separate 
		configuration file so that this file can easily be used
		for integration testing outside of an OSGi environment -->

	<bean name="myExampleBean"
		class="org.cytoscape.helloservice.internal.ExampleBeanImpl" />

</beans>

このBeanをOSGiサービスとしてエクスポートするには、bundle-context-osgi.xmlの方をいじります:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:osgi="http://www.springframework.org/schema/osgi"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                      http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd">

	<!-- definitions using elements of the osgi namespace can be included
		in this file. There is no requirement to keep these definitions
		in a separate file if you do not want to. The rationale for 
		keeping these definitions separate is to facilitate integration
		testing of the bundle outside of an OSGi container -->

	<osgi:service id="helloServiceOSGi" ref="myExampleBean"
		interface="org.cytoscape.helloservice.ExampleBean" />
</beans>

たったこれだけです。では実行してみましょう。プロジェクトのトップディレクトリで、コマンドラインから

mvn clean install pax:provision

とタイプします。psコマンドでhelloserviceのIDを確認(今回は10)。そしてサービスの状態を確認します:

-> services 10

org.cytoscape.helloservice (10) provides:
-----------------------------------------
Bundle-SymbolicName = org.cytoscape.helloservice
Bundle-Version = 1.0.0.SNAPSHOT
objectClass = org.cytoscape.helloservice.ExampleBean
org.springframework.osgi.bean.name = myExampleBean
service.id = 23
----
Bundle-SymbolicName = org.cytoscape.helloservice
Bundle-Version = <unknown value type>
objectClass = org.springframework.osgi.context.DelegatedExecutionOsgiBundleApplicationContext, org.springframework.osgi.context.ConfigurableOsgiBundleApplicationContext, org.springframework.context.ConfigurableApplicationContext, org.springframework.context.ApplicationContext, org.springframework.context.Lifecycle, org.springframework.beans.factory.ListableBeanFactory, org.springframework.beans.factory.HierarchicalBeanFactory, org.springframework.context.MessageSource, org.springframework.context.ApplicationEventPublisher, org.springframework.core.io.support.ResourcePatternResolver, org.springframework.beans.factory.BeanFactory, org.springframework.core.io.ResourceLoader, org.springframework.beans.factory.DisposableBean
org.springframework.context.service.name = org.cytoscape.helloservice
service.id = 24

たった今作ったバンドルが、org.cytoscape.helloservice.ExampleBeanと言うインターフェイスを窓口として、OSGiサービスとして公開されています。実装はこのサービスの利用者からは見えなくなっていて、実装とAPIがきれいに分離されています。これで成功です。

インターフェイス、実装ともに、OSGiはおろか、Springにも非依存のコードになっている事が確認していただけたと思います。

OSGiサービスを利用するバンドルを作製する

次はサービスを利用するバンドルを書きます。話を簡略化するため、インテグレーションテストについては考えないことにして、helloserviceuserバンドルのテストディレクトリの中にあるExampleBeanIntegrationTestと言うファイルを削除します。そして、このバンドルはhelloserviceの中のクラスを利用しますので、pom.xmlファイルに依存を明示しましょう:

<dependency>
  <groupId>org.cytoscape.hello</groupId>
  <artifactId>org.cytoscape.helloservice</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

これをpomの<dependencies>セクションの最後に追加します。

ではまず、OSGiサービスをインポートしましょう。bundle-context-osgi.xmlを書きます:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:osgi="http://www.springframework.org/schema/osgi"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                      http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd">

	<!-- definitions using elements of the osgi namespace can be included
		in this file. There is no requirement to keep these definitions
		in a separate file if you do not want to. The rationale for 
		keeping these definitions separate is to facilitate integration
		testing of the bundle outside of an OSGi container -->
	<osgi:reference id="helloServiceRef"
		interface="org.cytoscape.helloservice.ExampleBean" />

</beans>

これでSpring DMが、OSGiサービスレジストリの中から、ExampleBeanというインターフェイスを公開しているサービスを探して来て、helloServiceRefと言う名前のBeanとしてインポートします。

次に実際にサービスを利用するクラスを書きます:

package org.cytoscape.helloserviceuser.internal;

import org.cytoscape.helloservice.ExampleBean;

public class HelloConsumer {
  private ExampleBean helloService;

  public void setHelloService(ExampleBean helloService) {
    this.helloService = helloService;
    this.helloService.sayHello();
  }
}

この状態では、とりあえずhelloServiceが初期化された際に、sayHello()メソッドが実行されます。そして、こいつに先ほどのサービスをインジェクトします:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

	<!-- regular spring configuration file defining the beans for this
		bundle. We've kept the osgi definitions in a separate 
		configuration file so that this file can easily be used
		for integration testing outside of an OSGi environment -->

	<bean name="myExampleBean"
		class="org.cytoscape.helloserviceuser.internal.ExampleBeanImpl" />
	<bean name="helloServiceUser"
		class="org.cytoscape.helloserviceuser.internal.HelloConsumer">
		<property name="helloService" ref="helloServiceRef" />
	</bean>
</beans>

方法は、普通のBeanと全く同じです。Setter Injectionを使っています。

では実行します。

mvn clean install pax:provision
(略)
情報: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1884174: defining beans [helloServiceRef,myExampleBean,helloServiceUser]; root of factory hierarchy
Hello, OSGi World!
2008/07/08 19:27:20 org.springframework.osgi.context.support.AbstractOsgiBundleApplicationContext publishContextAsOsgiServiceIfNecessary
情報: Publishing application context as OSGi service with properties {org.springframework.context.service.name=org.cytoscape.helloserviceuser, Bundle-SymbolicName=org.cytoscape.helloserviceuser, Bundle-Version=1.0.0.SNAPSHOT}

無事にサービスが利用されています。サービスは自動的に同期され、サービスがユーザーの方に注入されています。Spring DMを使わないと、ここはServiceTrackerと言うOSGi依存のクラスを利用することになるのですが、そのあたりのことをSpringがやってくれます。

さて、ここからがOSGiのダイナミズムに関係する部分です。ここでユーザーのバンドルを停止しましょう:

START LEVEL 6
   ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.4)
[   1] [Active     ] [    5] Spring DM Extender (1.1.0)
[   2] [Active     ] [    5] Apache Commons Logging (1.1.1)
[   3] [Active     ] [    5] Spring Beans (2.5.5.A)
[   4] [Active     ] [    5] Spring Context (2.5.5.A)
[   5] [Active     ] [    5] Spring Core (2.5.5.A)
[   6] [Active     ] [    5] Spring DM Core (1.1.0)
[   7] [Active     ] [    5] AOP Appliance API (1.0.0)
[   8] [Active     ] [    5] Spring AOP (2.5.5.A)
[   9] [Active     ] [    5] Spring DM IO (1.1.0)
[  10] [Active     ] [    5] org.cytoscape.helloservice (1.0.0.SNAPSHOT)
[  11] [Active     ] [    5] org.cytoscape.helloserviceuser (1.0.0.SNAPSHOT)
[  12] [Active     ] [    1] osgi.compendium (4.0.1)
[  13] [Active     ] [    1] Apache Felix Shell Service (1.0.1)
[  14] [Active     ] [    1] Apache Felix Shell TUI (1.0.1)
-> stop 11
2008/07/08 19:30:17 org.springframework.osgi.context.support.AbstractOsgiBundleApplicationContext doClose
情報: Application Context service already unpublished
2008/07/08 19:30:17 org.springframework.context.support.AbstractApplicationContext doClose
情報: Closing org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext@17725c4: display name [OsgiBundleXmlApplicationContext(bundle=org.cytoscape.helloserviceuser, config=osgibundle:/META-INF/spring/*.xml)]; startup date [Tue Jul 08 19:27:19 PDT 2008]; root of context hierarchy
2008/07/08 19:30:17 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
情報: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1884174: defining beans [helloServiceRef,myExampleBean,helloServiceUser]; root of factory hierarchy
-> ps
START LEVEL 6
   ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.4)
[   1] [Active     ] [    5] Spring DM Extender (1.1.0)
[   2] [Active     ] [    5] Apache Commons Logging (1.1.1)
[   3] [Active     ] [    5] Spring Beans (2.5.5.A)
[   4] [Active     ] [    5] Spring Context (2.5.5.A)
[   5] [Active     ] [    5] Spring Core (2.5.5.A)
[   6] [Active     ] [    5] Spring DM Core (1.1.0)
[   7] [Active     ] [    5] AOP Appliance API (1.0.0)
[   8] [Active     ] [    5] Spring AOP (2.5.5.A)
[   9] [Active     ] [    5] Spring DM IO (1.1.0)
[  10] [Active     ] [    5] org.cytoscape.helloservice (1.0.0.SNAPSHOT)
[  11] [Resolved   ] [    5] org.cytoscape.helloserviceuser (1.0.0.SNAPSHOT)
[  12] [Active     ] [    1] osgi.compendium (4.0.1)
[  13] [Active     ] [    1] Apache Felix Shell Service (1.0.1)
[  14] [Active     ] [    1] Apache Felix Shell TUI (1.0.1)

停止したのが確認できます。そしてふたたび起動してみます:

-> start 11
2008/07/08 19:30:24 org.springframework.osgi.extender.support.DefaultOsgiApplicationContextCreator createApplicationContext
情報: Discovered configurations {osgibundle:/META-INF/spring/*.xml} in bundle [org.cytoscape.helloserviceuser (org.cytoscape.helloserviceuser)]
-> 2008/07/08 19:30:24 org.springframework.context.support.AbstractApplicationContext prepareRefresh
情報: Refreshing org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext@1e13e07: display name [OsgiBundleXmlApplicationContext(bundle=org.cytoscape.helloserviceuser, config=osgibundle:/META-INF/spring/*.xml)]; startup date [Tue Jul 08 19:30:24 PDT 2008]; root of context hierarchy
2008/07/08 19:30:24 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from URL [bundle://11.0:0/META-INF/spring/bundle-context-osgi.xml]
2008/07/08 19:30:24 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from URL [bundle://11.0:0/META-INF/spring/bundle-context.xml]
2008/07/08 19:30:24 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
情報: Bean factory for application context [org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext@1e13e07]: org.springframework.beans.factory.support.DefaultListableBeanFactory@15718f2
2008/07/08 19:30:24 org.springframework.osgi.extender.internal.dependencies.startup.DependencyWaiterApplicationContextExecutor stageOne
情報: No outstanding OSGi service dependencies, completing initialization for OsgiBundleXmlApplicationContext(bundle=org.cytoscape.helloserviceuser, config=osgibundle:/META-INF/spring/*.xml)2008/07/08 19:30:24 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
情報: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@15718f2: defining beans [helloServiceRef,myExampleBean,helloServiceUser]; root of factory hierarchy
Hello, OSGi World!
2008/07/08 19:30:24 org.springframework.osgi.context.support.AbstractOsgiBundleApplicationContext publishContextAsOsgiServiceIfNecessary
情報: Publishing application context as OSGi service with properties {org.springframework.context.service.name=org.cytoscape.helloserviceuser, Bundle-SymbolicName=org.cytoscape.helloserviceuser, Bundle-Version=1.0.0.SNAPSHOT}

helloserviceuserバンドル再起動されたのと同時に、Springが再びサービスを探しだし、自動的にユーザーへ注入しました。サービスがきちんと使われているか確認してみます:

-> services -u 11

org.cytoscape.helloserviceuser (11) uses:
-----------------------------------------
objectClass = org.xml.sax.EntityResolver
service.id = 22
----
objectClass = org.springframework.beans.factory.xml.NamespaceHandlerResolver
service.id = 21
----
Bundle-SymbolicName = org.cytoscape.helloservice
Bundle-Version = 1.0.0.SNAPSHOT
objectClass = org.cytoscape.helloservice.ExampleBean
org.springframework.osgi.bean.name = myExampleBean
service.id = 23

大丈夫ですね。では、サービスそのものを停止してしまったらどうなるんでしょう?

-> stop 10
2008/07/08 19:36:50 org.springframework.osgi.context.support.AbstractOsgiBundleApplicationContext doClose
情報: Application Context service already unpublished
2008/07/08 19:36:50 org.springframework.context.support.AbstractApplicationContext doClose
情報: Closing org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext@a4e743: display name [OsgiBundleXmlApplicationContext(bundle=org.cytoscape.helloservice, config=osgibundle:/META-INF/spring/*.xml)]; startup date [Tue Jul 08 19:27:19 PDT 2008]; root of context hierarchy
2008/07/08 19:36:50 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
情報: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@bac9b9: defining beans [helloServiceOSGi,myExampleBean]; root of factory hierarchy
2008/07/08 19:36:50 org.springframework.osgi.service.exporter.support.OsgiServiceFactoryBean unregisterService
情報: Unregistered service [ServiceWrapper for org.apache.felix.framework.ServiceRegistrationImpl@1142196]
-> services -u 11

org.cytoscape.helloserviceuser (11) uses:
-----------------------------------------
objectClass = org.xml.sax.EntityResolver
service.id = 22
----
objectClass = org.springframework.beans.factory.xml.NamespaceHandlerResolver
service.id = 21
->

サービスの提供元が停止すると、そのサービスの利用も停止しました。このような同期を(かなりの部分)自動的に面倒を見てくれるのがSpring DMです。

何が便利なのか、これだけでは実感がわかないかもしれませんが、OSGiAPIに依存しているコードも書いてみて、比較してみると、サービスやオブジェクトのワイアリングを外に出したこのバージョンの方が、コードがスッキリしていると感じられると思います。

ここで書かれたクラス群は、全てPOJOです。従って、他の環境で利用することもそれほど大変ではありません。また、OSGiバンドルと言っても、要するにただのJARファイルにメタデータを加えただけですから、普通のライブラリとして再利用することも勿論可能です。こういった特長が、このフレームワークで作製されたコンポーネントの再利用性を高めるポイントになっています。