OSGi enRoute – 2.3_3 – Provider Bundle 工程
在本章中你将会学到什么
在本章中,我们会创建另外一个提供Eval API实现的工程, 即所谓的Provider。在本例中我们将其命名为simple.provider。我们将会从Provider Bundle中Export对应的api Package,并且解释为何这样做会简化很多事情。
确保你在顶层文件夹中:
1 |
$ cd ~/workspaces/osgi.enroute.examples.eval |
关于 Service
Service是一个由Bundle注册的对象。它会注册在一个接口和多个属性之下。Service是动态的,意味着我们必须在编写使用Service的代码时尽量保守。我们使用Declarative Services 注解来大大简化了使用Service的工作。
创建一个Provider工程
在之前的章节中我们创建了一个用于提供表达式计算器的API。我们现在需要一个此服务的Provider。一个Provider负责定义在API中的契约,使得Consumer可以使用对应的服务。在Eval Service的例子中,Consumer将会调用eval(String)方法,而Provider必须实现该方法。
在 osgi.enroute.examples.eval 文件夹中,我们创建了一个名为simple.provider的文件夹。在这个文件夹中我们创建了POM。
1 2 3 |
osgi.enroute.examples.eval $ mkdir simple.provider osgi.enroute.examples.eval $ cd simple.provider simple.provider $ |
POM
1 2 |
simple.provider $ vi pom.xml // fill in from next section |
1 2 3 4 5 6 |
<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/xsd/maven-4.0.0.xsd" > <modelVersion>4.0.0</modelVersion> |
对应的parent和packaging元素与API工程中的一样:
1 2 3 4 5 6 |
<parent> <groupId>org.osgi</groupId> <artifactId>osgi.enroute.examples.eval</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <packaging>jar</packaging> |
在之前的章节中,我们学习过工程名称的最后一段定义了工程的类型;这同样在OSGi enRoute模板中得到支持。 一个Provider工程因此必须以.provider为名称的后缀。工程的命名我们一般以工作空间的名称、Service API名称以及一个能表示这是何种服务的实现的词语作为开头。下面来实现一个非常简单的服务实现,因此对应的名称可以为:osgi.enroute.examples.eval.simple.provider。因此POM我们需要有如下内容:
1 2 |
<artifactId>osgi.enroute.examples.eval.simple.provider</artifactId> <description>Eval Provider</description> |
我们继承了OSGi enRoute的基础API(一系列标准化并且特殊的服务契约,以便于开发Web App),不过在本例中我们需要将API工程作为dependency:
1 2 3 4 5 6 7 8 |
<dependencies> <dependency> <groupId>org.osgi</groupId> <artifactId>osgi.enroute.examples.eval.api</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> </project> |
服务实现的源码
下面我们需要增加一个实现类。在本例中,我们使用正则表达式来作为解析器,以便在文件src/main/java/osgi/enroute/examples/eval/provider/EvalImpl.java中实现一个非常简单的表达式解析器:
1 2 |
simple.provider $ mkdir -p src/main/java/osgi/enroute/examples/eval/provider simple.provider $ vi src/main/java/osgi/enroute/examples/eval/provider/EvalImpl.java |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package osgi.enroute.examples.eval.provider; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.log.LogService; import osgi.enroute.examples.eval.api.Eval; @Component(name = "osgi.enroute.examples.eval.provider") public class EvalImpl implements Eval { Pattern EXPR = Pattern.compile( "\\s*(?<left>\\d+)\\s*(?<op>\\+|-)\\s*(?<right>\\d+)\\s*"); @Reference LogService log; @Override public double eval(String expression) throws Exception { Matcher m = EXPR.matcher(expression); if ( !m.matches()) { log.log(LogService.LOG_WARNING, "Invalid expression " + expression); throw new IllegalArgumentException("Invalid expression " + expression); } double left = Double.valueOf( m.group("left")); double right = Double.valueOf( m.group("right")); switch( m.group("op")) { case "+": return left + right; case "-": return left - right; } return Double.NaN; } } |
由于增加了@Component注解,这个类被设置为了Declarative Service(DS)服务Component。如果你的Component类实现了一个或多个接口,则这些接口会被自动注册为OSGi Service。因此在本例中,我们希望实现Eval接口并将其注册为Eval Service。
由于我们将org.osgi:osgi.enroute.examples.eval.api:1.0.0-SNAPSHOT 工程添加到了依赖之中,因此可以使用Eval接口。
Bnd文件
如果要生成一个合适的Bundle,我们需要在POM的相同文件夹中生成一个bnd.bnd文件。此文件内容如下:
1 2 |
simple.provider $ vi bnd.bnd // fill in content from next section |
1 2 3 4 5 6 7 |
# # OSGi enRoute Eval Example # Bundle-Description: \ Provides a simple implementation for an eval parser Export-Package: osgi.enroute.examples.eval.api |
导出(Export) API
细心的读者应该已经发现了osgi.enroute.examples.eval.api 包被Export了,但是它并不是这个工程的一部分。这是bnd提供的很有用的一个小技巧。任何在Export-Package 或Private-Package Header中列出的包都会从classpath中复制出来,即使它们不是工程的一部分也是如此。
不过这样带来了一个问题,为什么API还被包含在Bundle之中?难道API不应该被放到单独的Bundle之中吗?
我们从Provider Bundle中Export API的原因是这样使得构建可执行文件更加简单,而且这样并不会带来任何隐性成本。没有隐性成本的原因是因为它的兼容性。正如我们在语义化版本控制章节所述,一个Provider与它的API没有后向兼容性。在API中的任何改变都需要改变Provider的代码,以确保Provider会实现服务契约所需的内容。
因此,将API与其Provider隔离开没有什么作用,因为一个给定的Provider几乎总是使用相同的API版本。使用这个版本的API会依赖更少的Bundle,并且更不容易造成metadata中的错误。
不过,OSGi专家们并不完全同意这些观点。
构建
用Maven的术语来说我们已经定义了一个多模块(Multi-Module)工程,目前它包含两个Module;api和simple.provider。这些Module应该在一个aggregator pom中进行定义,而这经常在Parent pom中进行。我们应该在父文件夹中的pom.xml中添加一段内容。
1 2 3 |
simple.provider $ cd .. osgi.enroute.examples.eval $ vi pom.xml //在<properties>部分之前,填入下面的内容 |
1 2 3 4 |
<modules> <module>api</module> <module>simple.provider</module> </modules> |
我们现在有了充分的工程信息来构建Bundle。由于我们修改了Parent pom,最好进行一次clean build。然后我们就可以看看Module(Bundle)到底是怎样的情况了。注意确保你处在顶层文件夹中!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
osgi.enroute.examples.eval $ mvn clean install ... osgi.enroute.examples.eval $ cd simple.provider simple.provider $ bnd print target/osgi.enroute.examples.eval.simple.provider-1.0.0-SNAPSHOT.jar [MANIFEST osgi.enroute.examples.eval.provider-1.0.0-SNAPSHOT] Bnd-LastModified 1474989660493 Build-Jdk 1.8.0_25 Built-By aqute Bundle-Description Provides a simple implementation for an eval parser Bundle-ManifestVersion 2 Bundle-Name osgi.enroute.examples.eval.simple.provider Bundle-SymbolicName osgi.enroute.examples.eval.simple.provider Bundle-Version 1.0.0.201609271521 Created-By 1.8.0_25 (Oracle Corporation) Export-Package osgi.enroute.examples.eval.api;version="1.0.0" Import-Package osgi.enroute.examples.eval.api;version="[1.0,1.1)" Manifest-Version 1.0 Private-Package osgi.enroute.examples.eval.provider Provide-Capability osgi.service;objectClass:List<String>="osgi.enroute.examples.eval.api.Eval" Require-Capability osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))" Require-Capability osgi.extender;filter:="(&(osgi.extender=osgi.component)(version>=1.3.0)(!(version>=2.0.0)))",osgi.service;filter:="(objectClass=org.osgi.service.log.LogService)";effective:=active,osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))" Service-Component OSGI-INF/osgi.enroute.examples.eval.provider.xml Tool Bnd-3.3.0.201609221906 [IMPEXP] Import-Package org.osgi.service.log {version=[1.3,2)} osgi.enroute.examples.eval.api {version=[1.0,1.1)} Export-Package osgi.enroute.examples.eval.api {version=1.0.0, imported-as=[1.0,1.1)} |
通过这样我们可以看到Provider Bundle具有如下布局:
● Private packages – 仅在Bundle内部可用的包。其他的Bundle可以有同名但内容不同的包。
● Exported packages –提供给其他Bundle的包,需要及时维护它们。
● Imported packages – 期望其他Bundle Export的包,期望它的作者也会及时维护它们。
如下图所示:
它是如何工作的?
消费者(Consumer)和提供者(Provider)的概念很容易混淆,主要是因为接口的实现者和客户端常常被混淆。然而,在Service包中,一个Service API的Provider可以同时是接口的实现和客户端。一个Provider负责提供契约的价值,一个Consumer则接收契约的价值。我们需要区分出这两者的原因在于他们对Bundle的打包和版本控制有广泛的影响。
比如你从我手里买了一栋房子。在这个场景中,你是契约中的Consumer而我是Provider。令人惊讶的是这些角色并非对称的。例如,如果卖家在契约签订完毕后增加了一个额外的房间,买家是不会反对的(OK,你应该已经知道我想说什么了)。但是如果卖家移除了一个房间则买家会不满。一个Consumer可以期望向后兼容性,但是Provider会跟契约绑定。几乎Service契约中的所有改变都会需要Provider更新来提供新的功能。
因此一个Consumer相对来说离契约更远,它可能会在多个不同的Service契约中扮演Consumer的角色。一个Provider通常仅提供一个Service契约,同时它可以在其它契约中扮演Consumer角色。
因此在OSGi中,Provider的最佳实践是包含其Service API代码并且将它们Export出来。将API和Provider隔离开没有多大意义,因为Provider和API往往是一一对应的,这与Consumer会从API获得向后兼容性不同。将API放在实现Bundle之中会让事情变得更加简单。即便如此,也不要将它们放到同一个工程之中,因为编译会需要包含接口的实现的JAR。编译应该仅仅依赖带有API的JAR包,避免不小心依赖实现代码。
(译者注:这部分还需要推敲)
我们学到了什么?
在本章中,我们为Eval Service API创建了一个简单的Provider Bundle。我们在这个Bundle中Exprot了Eval Service API,同时也Import了它。

