创建OSGi Hello World工程里,我们提到OSGI通过不同的classloader来加载不同的bundle,达到隔离bundle的目的,我们也提到通过Import/Export Package的方式来控制bundle之间“有限地”访问对方的类。

这里我们只有一个bundle(demo1-1.0.jar),但我们在这个bundle的manifest.mf里,可以看到设置了Import-Package和Export-Package。

Import package/Export package

在OSGI里,bundle暴露自己的类(Export)或引用其他bundle的类(Import)的单位是package(就是每个java代码文件最上头定义的Package),也就是对于bundle来说,只能将整个package里的类暴露或引用进来,而不是对单个类作暴露或引用,所以,我们在设计时,应考虑将需暴露出去的类放在一个或几个package里,而将需隐藏起来的类放在其他的package里。

然后,我们在manifest.mf里加入诸如:

Export-Package:com.ponder.Demo.demo1

这样的项,那么,com.ponder.Demo.demo1这个package里的类就可以被其它bundle引用了。

而需要引用这个package的其它bundle,则需要在它们的manifest.mf里加入:

Import-Package:com.ponder.Demo.demo1

才能引用这个package里的类,否则就会出现“ClassNotFound"这样的异常。

另外,我们还可以对package定义一个版本号,例如:

Export-Package:com.ponder.Demo.demo1;version="1.0"

而在引用时,也可以指定版本号:

Import-Package:com.ponder.Demo.demo1;version="1.0"

甚至,在引用时,我们可以指定版本号的区间:

Import-Package:com.ponder.Demo.demo1;version="[1.0,2.0)"

上面的引用就指定了要引用的package版本号必须在1.0(含)和2.0(不含)之间。

那么,指定版本号有什么好处呢?

在传统的Java应用中,因为jar包通常都是在同一个classloader里加载,所以,如果两个jar包里都包含一个同名的类(pacakge一样,类名也一样)的话,就会出现冲突,导致后加载的jar包无法加载。

但是在OSGI环境下,由于classloader的隔离,所以同名的类是可以同时加载在一个JVM上的。我们给不同的bundle export的package设置版本号,就可以区分开这些package。那么,我们就可以根据需要引用指定的package,而不是随机地发现其中的一个(通常是会引用到先加载的那个)。

如果在一个平台上需更新bundle,我们也可以利用这样的方式,分别对新旧两个bundle进行“热切换”。当然,“热切换”并不是那么简单,还会涉及更多的机制,这个我们可以日后再详细讨论。

OSGi服务:发布服务

OSGI framework提供了一个osgi service registry,我们可以将一个java bean注册到这个osgi service registry上,然后,其它的bundle就可以通过osgi service registry来发现和引用这个Java bean,而这个Java bean就是一个OSGI Service(OSGI服务)了。

我们新建了一个demo2的示例代码,先看看demo2的代码结构:

demo2-1.png

其中:

3 是一个接口ICalculation,我们将它放在com.ponder.Demo.demo2.contract这个package下。

2 是ICalculation的一个实现。

1 是一个Activator,下面是它的代码:

package com.ponder.Demo.demo2;

import com.ponder.Demo.demo2.Impl.Calculation;
import com.ponder.Demo.demo2.contract.ICalculation;
import java.util.HashMap;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

/**
 *
 * @author han
 */
public class activator implements BundleActivator {

    @Override
    public void start(BundleContext context) throws Exception {
        Dictionary<String, String> props = new Hashtable<String, String>();
        props.put("ServiceName", "Calculation");
        context.registerService(ICalculation.class.getName(), new Calculation(), props);
        System.out.println("Service registered!");
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        System.out.println("Stop demo2 bundle!");
    }
}

代码中,我们定义了一个Hashtable(Dictionary的子类) props,这个称为“服务属性”,服务属性是一组键值对,每个服务都可以根据需要设置0到n个服务属性,服务属性的用处,我们后面会再介绍。

然后,我们用接口名、实现的实例(instance)和服务属性作为参数,通过BundleContext的registerService的方法将这个实现注册到OSGI service registry上。

将项目编译打包成bundle后,部署到ServiceMix上:

demo2-2.png

通过list命令可以看到demo2的bundle ID是212,再用命令

karaf@root> ls 212

可以看到demo2提供了一个osgi服务(服务接口为com.ponder.Demo.demo2.contract.ICalculation),服务ID为361,而且还列出了服务属性:ServiceName = Calculation

OSGi服务:引用服务

有了OSGI服务后,我们就可以在另一个Bundle引用它。

我们先看看demo3 activator的start方法:

@Override
    public void start(BundleContext context) throws Exception {
         ServiceReference[] refs = context.getServiceReferences(ICalculation.class.getName(), "(ServiceName=Calculation)");
         if(refs!=null && refs.length>0){
             ICalculation service=(ICalculation)context.getService(refs[0]);
             System.out.println("1+1="+service.add(1, 1));
             System.out.println("2-1="+service.sub(2, 1));
             System.out.println("2*3="+service.sub(2, 3));
         }
    }

代码的第3行,通过Bundle Context的getServiceReferences方法获得实现服务接口ICalculation的服务引用,可能有多个。

这个方法有两个参数:

参数1是ICalculation的接口名(com.ponder.Demo.demo2.contract.ICalculation),这个和demo2注册服务时对应的服务接口是一致的;

参数2是一个表达式,它是和服务属性相关的,通过它,我们可以根据服务属性获得特定的OSGI服务,而不是所有实现该接口的OSGI服务;

参数2可以如以下的形式:

"(ServiceName=Calculation)"

"&((ServiceName=Calculation)(ServiceType=Math))"

得到服务引用后,我们用Bundle Context的getService的方法得到其中的服务,并使用它。

在demo3里,我们就取用了第一个符合条件的服务;

在demo3的activator的代码里,我们import了demo2的com.ponder.Demo.demo2.contract.ICalculation这个接口类,而这个接口类是在demo2 bundle定义的,所以,如果不做处理,demo3是找不到这个类的。

所以,我们需要在demo2生成的bundle里面的META-INF/Mainifest.mf里将ICalculation所在的package(com.ponder.Demo.demo2.contract) export出来;然后在demo3生成的bundle里面的META-INF/Mainifest.mf里将这个package import进来。

我们是借助maven-jar-plugin,在pom.xml里分别指定:

demo2:留意第15行

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:pom="http://maven.apache.org/POM/4.0.0">
...
<build>
<plugins>
...
<plugin>
...
<artifactId>maven-jar-plugin</artifactId>
...
<configuration>
<archive>
...
<manifestEntries>
<Export-Package>com.ponder.Demo.demo2.contract;version="1.0"</Export-Package>
</manifestEntries>
</archive>
</configuration>
</plugin>
...
</plugins>
</build>
...
</project>

demo3:留意第15行

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:pom="http://maven.apache.org/POM/4.0.0">
...
<build>
<plugins>
...
<plugin>
...
<artifactId>maven-jar-plugin</artifactId>
...
<configuration>
<archive>
...
<manifestEntries>
<Import-Package>org.osgi.framework,com.ponder.Demo.demo2.contract;version="[1.0,2.0)"</Import-Package>
</manifestEntries>
</archive>
</configuration>
</plugin>
...
</plugins>
</build>
...
</project>

在打包时,maven就会在jar包里的META-INF/Mainifest.mf分别添加:

demo2:

Export-Package:com.ponder.Demo.demo2.contract;version="1.0"

demo3:

Import-Package:org.osgi.framework,com.ponder.Demo.demo2.contract;version="[1.0,2.0)"

细心的同学或者会留意到demo2的Export-Package时,在package后加了一个版本号,而demo3的Import-Package则对package指定了一个版本号的区间[1.0,2.0)。这里表示的是demo3只Import版本号>=1.0,而且版本号<2.0的package。

我们尝试跑一下demo3,首先,我们先将/deploy文件夹清空,把/data文件夹整个删除,然后将demo2-1.0.jar复制到/deploy文件夹下,运行/bin/servicemix;

这时,我们只部署了demo2 bundle;通过ls 可以看到demo2注册了一个OSGI服务;

我们再将demo3-1.0.jar复制到/deploy文件夹下,这时servicemix的console上就可以看到:

karaf@root>
1+1=2
2-1=1
2*3=6

登录发表评论 注册

killko

和maven无关,osgi里的服务启动顺序是不确定的,所以通常基于blueprint或declarative service等来组装这些服务,构建出更粗粒度的服务@TSOL

TSOL

我第一次运行得时候,是正常的,后来运行老是输出找不到服务,好像是服务启动先后顺序得问题,被引用服务在引用者之后启动了。我得代码结构:intellij maven 父子多模块 方式开发的,不知道有没有影响。

QQ截图20170103163929.png

反馈意见