Export-Package用于声明Bundle要导出哪些Package,Import-Package用于声明Bundle需要导入哪些Package。这两个标记最简单的方式是直接跟随导入或导出的Package名称,如果导入或导出多个Package,则使用逗号分隔,如下所示:
Export-Package: org.osgi.service.io, org.osgi.service.web
Import-Package: org.osgi.service.io, org.osgi.service.web
这种写法是OSGi中导入导出Package的基本用法,也是最常见用法,已经能满足相当多的应用场景,但是在开发过程中总会遇见各种不同的需求,要根据特定规则去选择适合的Package。下面对OSGi规范中定义的几种对导入导出进行筛选的过滤方式进行介绍。
1.根据类名过滤
如果仅在Package层次上控制导出的粒度不够精细,无法满足应用需求,那么可以使用附加参数include和exclude进一步描述要导出Package中哪一部分内容。比如,只希望导出org.osgi.service.io包中命名以“IO”开头的接口,不导出实现类(假设实现类都以Impl结尾),那么可以这样写:
Export-Package: org.osgi.service.io;include="IO*"; exclude:="*Impl"
include和exclude参数的具体使用方法如下:
附加参数include用于列举需要导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只有符合规则的类才会被导出。
附加参数exclude用于列举禁止导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只要符合规则的类就不会被导出。
include和exclude的限制是在导出时处理的,导入时无需对应用做任何特殊声明,Import-Package标记也无法与这两个参数搭配使用。例如与前面配对的导入声明依然为:
Import-Package: org.osgi.service.io
2.根据版本过滤
在OSGi系统中,同一个名称的Package可能存在多个不同版本。假设Bundle C开发时引入了Spring 2.0版的Package,并且使用了某些只在这个版本私有的特征,而Bundle D开发时使用的是Spring 3.0版的Package,那么从这个系统中导出Spring的Bundle就必须明确指明Spring的版本号,以便导入时区分。示例如下:
Bundle A(导出Spring 2.0):
Export-Package: org.springframework.core; version="2.0.0"
Bundle B(导出Spring 3.0):
Export-Package: org.springframework.core; version="3.0.0"
对应的,导入时也要指明版本,准确地说应指明某个版本范围,例如:
Bundle C(导入Spring 2.x版):
Import-Package: org.springframework.core; version="[2.0.0,2.9.9]"
Bundle D(导入Spring 3.0以上版本):
Import-Package: org.springframework.core; version="3.0.0"
这里要注意我们导入的是“版本范围”而不是某个具体的“版本”,例如示例中Bundle D的Import-Package写法,只声明了version="3.0.0"的含义并不是“只导入”3.0.0版本的Package,而是导入3.0.0或以上版本的Package,因为这更符合一般的开发使用场景。如果需要指定只导入3.0.0版的Package,需要这样写:
Bundle E(只导入Spring 3.0版本):
Import-Package: org.springframework.core; version="[3.0.0,3.0.0]"
version参数的具体使用方法是:在导出Package时,此参数声明导出Package的版本号,如果不设置,默认值为0.0.0;在导入Package时,此参数声明要导入Package的版本范围,如果不设置,默认值为[0.0.0, ∞)。在声明版本范围时,方括号“[”和“]”表示“范围包含此数值”,圆括号“(”和“)”表示“范围不含此数值”。
在OSGi R3.x及之前的版本中,version标记原本叫做specification-version,在R4.x规范中新增了version标记,但依然保留了specification-version这个别名,但是已将它声明为Deprecated,如果同时设置了version和specification-version的值,那么这两个值必须相等。
3.根据提供者过滤
OSGi还允许开发人员根据Bundle名称、版本等信息来过滤Package,这种过滤方式在规范中被称为“选择提供者(Provider Selection)”。由开发人员明确指明导入的Package必须来自某个提供者的做法在实际开发之中是很少见的,它会增加系统的兼容性风险和人为的不确定因素。就如同我们组装电脑选择显示卡一样,理性的选择方式是根据性能需求确定显示芯片、显存大小和位宽等参数,再来选择合适的产品,而不是明确要求必须选择某公司出产的某型号产品。
根据提供者过滤一般使用在测试Bundle的时候,Import-Package标记提供了两个附加参数bundle-symbolic-name和bundle-version来完成这项功能,它们分别用于对Bundle的名称和版本进行过滤,示例如下:
Bundle A:
Bundle-SymbolicName: BundleA
Import-Package: com.acme.foo; bundle-symbolic-name="BundleB";
bundle-version="[1.4.1,2.0.0)"
Bundle B
Bundle-SymbolicName: BundleB
Bundle-Version: 1.4.1
Export-Package: com.acme.foo
上面配置指明一定要是来自于名称为“BundleB”并且版本在1.4.1至2.0.0之间的com.acme.foo包才会被选择。
bundle-symbolic-name和bundle-version参数的具体使用方法如下。
附加参数bundle-symbolic-name:参数值为某个Bundle的名称,只有符合该名称的Bundle所导出的Package才会被导入。
附加参数bundle-version:参数值为导入Bundle(注意不是Package)的版本范围,只有版本在该范围之内的Bundle所导出的Package才会被导入。
4.根据属性过滤
导入和导出的Package除了使用include、exclude对类名进行过滤,使用version对版本进行过滤和使用bundle-symbolic-name、bundle-version对提供者信息进行过滤外,还有第四种方式:使用自定义的扩展属性进行过滤。包名、类名和版本这几项信息都是很客观的数据,在代码开发完成那一刻就已确定下来,不可能随意改变。而自定义的扩展属性可以满足某些根据开发人员自己加入的属性进行过滤的需求。示例如下:
Export-Package: org.osgi.simple;filter="true";
在导出org.osgi.simple时加入了自定义属性filter,它的值为“true”(属性值按照字符串处理),那下面三句Import-Package中,只有第Bundle B、C可以成功导入前面发布的org.osgi.simple包,而Bundle A中由于自定义属性冲突导致而导致匹配失败,示例如下:
Bundle A:
Import-Package: org.osgi.simple; filter="false"
Bundle B:
Import-Package: org.osgi.simple; filter="true"
Bundle C
Import-Package: org.osgi.simple;
注意,Bundle C虽然没有声明自定义属性filter,但在默认情况下这并不会产生匹配冲突。如果要改变这种情况,可以使用mandatory附加参数,强制要求必须存在扩展属性才能成功匹配,示例如下:
Export-Package: org.osgi.simple; filter="true";mandatory:="filter"
这样,在下面两句Import-Package中,只有第一句能成功导入前面发布的“org.osgi.simple”包,因为它发布时filter属性被声明为“必须”的,示例如下:
Bundle A:
Import-Package: org.osgi.simple; filter="true"
Bundle B:
Import-Package: org.osgi.simple
如果没有在mandatory中指定属性名称,那这种属性被默认为“optional”,即可选的,在导入时没有声明这个属性也不影响正常导入。
mandatory参数的具体使用方法:只适用于Export-Package,用于声明哪些属性是必选的,只有导入Package时提供了必选的属性,才能正确匹配到导出的Package。多个属性用逗号分隔,用双引号包裹。
5.可选导入与动态导入
在大多数情况下,导入某个Package,就说明当前这个Bundle的正常运行是必须依赖导入的Package的,比如一个基于Spring开发的程序,没有导入Spring的Package就肯定无法运行。但还有另外一些场景,导入某个Package是为了实现一些不影响Bundle正常运行的附加功能,比如为某个英文软件开发了一个实现中文语言支持的插件,不导入这样的Package也不应当影响整个系统正常运行,只不过软件仍以英文形式显示而已。
Import-Package标记也可以很好地支持上述需求,它有一个名为resolution的附加参数,用于说明一个依赖的Package是否是必需的。示例如下:
Bundle A:
Import-Package: org.osgi.simple;resolution:="mandatory"
Bundle B:
Import-Package: org.osgi.simple
Bundle C:
Import-Package: org.osgi.simple; resolution:="optional"
在上面例子中,Bundle A和Bundle B的Import-Package是等价的,因为resolution的默认值就是mandatory。在这个设置下,如果没有任何Bundle导出org.osgi.simple包,那么Bundle A 和Bundle B将会解析失败。而Bundle C不会,因为它明确指出了这个Package的导入是可选的(resolution:="optional")。
resolution参数的具体使用方法是这样的:附加参数resolution只适用于Import-Package标记,用于确定某个Package是可选的还是必须的。
还有一种场景,可能某个被导入的Package确实是必需的,但是提供这个Package的Bundle并不会在系统启动时就被安装。在这种情况下,只有真正使用到这个Package的类时,才会去导入这个Package,而不是在启动时就查找是否有提供Package的Bundle存在。
DynamicImport-Package标记可以处理这样的需求,这个标记的语义和Import-Package标记很类似,不同点在于DynamicImport-Package标记不在Bundle的解析阶段进行处理,无论它要求导入的Package是否存在,都不会影响Bundle的解析,示例如下:
Bundle A:
Import-Package: org.osgi.simple
Bundle B:
DynamicImport-Package: org.osgi.simple
如果没有任何Bundle提供org.osgi.simple包,那么Bundle A将无法解析,而Bundle B不受影响;但是如果真正使用到org.osgi.simple中的类时还是没有任何Bundle可提供,那么Bundle B依然要抛出ClassNotFoundException异常。
DynamicImport-Package也可是使用version附加参数来过滤导入Package的版本,在导入Package时声明属性,以便符合Package导出时mandatory参数中声明必须存在属性。
与Import-Package有所不同的是,DynamicImport-Package可以使用通配符,举一个极端的例子:
DynamicImport-Package: *
如果某个Bundle的元数据信息中有上面这行定义,那么它就成了一个“管理员级别”的Bundle,它可以访问整个系统中所有被导出过的Package。这样做很方便,却违背了OSGi中提倡的封装与按需使用的原则,会带来许多遗患,因此这并不是一种好的使用方式。
动态导入和可选导入实现的功能有些类似,它们的共同特征是在Bundle解析期间即使找不到要导入的依赖,也不会导致解析失败。它们的区别是,动态导入每次加载包中的类都会尝试去查找动态导入包,而可选导入包只有在Bundle解析时才进行连接尝试。
6.导出Package的依赖限制
如果导出Package的全部依赖都集中在一个Bundle之中,那么这个Package的导出是完全不受限制的。但是如果要导出的Package中有某些类还依赖于其他Bundle所提供的类,比如从其他Bundle所导出的Package中的类继承,或者其他Bundle的类出现在方法的声明中,在这种情况下,要导出这个Package就会受到依赖限制,必须先保证依赖的Package是可用的,才能保证导出的Package是可用的。
这种Package之间的关系可以通过在Export-Package标记中的uses附加参数来描述。例如:包org.osgi.service.http使用了包javax.servlet.http中的API,因此这两个包存在依赖关系。在导出org.osgi.service.http的Export-Package标记中就应当包含值为javax.servlet的uses参数,如下所示:
Export-Package: org.osgi.service.http;uses:="javax.servlet.http"
当一个系统中同时存在不同版本的Package时,uses参数对于协调依赖关系很有用。比如上面例子中的javax.servlet.http包同时存在2.4和2.1两个版本,而Bundle A导入的是2.4版,如下所示:
Bundle A:
Export-Package: org.osgi.service.http;uses:="javax.servlet.http"
Import-Package:javax.servlet.http; version="2.4"
对于任意一个Bundle,只要导入Bunlde A中发布org.osgi.service.http包,同时又导入了javax.servlet.http包,即使在导入的时候不明确指明其版本,OSGi实现也必须保证它只会使用2.4版的javax.servlet.http,如下所示:
Bundle B:
Import-Package:org.osgi.service.http // 由Bundle A提供
,javax.servlet.http // 这里默认会导入2.4版的Package
uses参数的具体使用方法是:附加参数uses只适用于Export-Package标记,用于说明导出Package的依赖关系。如果同时依赖多个Package,使用逗号分隔,并用双引号包裹。
当OSGi容器中的Bundle不断增加,依赖关系逐渐变得复杂时,uses参数就是协调依赖的必要手段,也是配置OSGi依赖的难点之一。在2.4.2节中我们还会对如何使用uses参数协调Package依赖约束进行更详细介绍。
7.导入整个Bundle
前面几项介绍的都是Package级别的导入、导出和依赖,OSGi还可以支持Bundle级别的依赖关系。我们可以使用Require-Bundle标记来声明要导入这个Bundle,如果成功导入了这个Bundle,那就意味着导入了这个Bundle中所有声明为导出的Package。
Require-Bundle标记后面应跟随一个或多个其他Bundle的名称(由Bundle-SymbolicName定义的名称),多个名称之间使用逗号分隔,如下所示:
Require-Bundle:BundleA、BundleB、BundleC
如果导入了某个Bundle,就可以重复导出该Bundle中已导出过的Package,就如同导出自己的Package一样,例子如下所示:
Bundle A:
Require-Bundle: BundleB
Export-Package: p
Bundle B:
Export-Package: p;partial=true;mandatory:=partial
这时如果有Bundle C要导入Package p,而又没有声明属性partial,那它将会从Bundle A中导入Package p。实际上Bundle A只承担了转发的工作,真正的Package p在Bundle B之中。这种情况属于拆分包的一个特例,应尽可能避免。
注意 拆分包(Split Packages)是指OSGi容器中有两个或两个以上的Bundle导出了相同名称的Package。容器存在拆分包会令包导入过程变得复杂,因为只带有包名的Import-Package标识无法保证能正确导入到所需的Package。必须通过过滤或者使用Require-Bundle才能导入正确的Package。
与Import-Package类似,Require-Bundle标记也有附加参数bundle-version和resolution。bundle-version用于过滤导入Bundle的版本,默认值为[0.0.0,∞),即不限制版本范围。resolution用于确定导入的Bundle是否是必需的,可选值为mandatory和optional,默认值为mandatory,含义与Import-Package标记的resolution参数相同,这里就不再详细举例介绍了。
在默认设置下,导入了某个Bundle,仅表示在本Bundle中可以访问被导入Bundle中声明导出的Package,除非明确用Export-Package声明过,否则这些Package在本Bundle中默认不会再次被导出。如果有必要,可以使用Import-Package标记的visibility附加参数来改变这种行为,示例如下:
Bundle A
Require-Bundle: BundleB;visibility:=reexport
Bundle B
Export-Package: org.osgi.service.http
在上面例子中由于明确设置了visibility的值为reexport,Bundle A中就会重复导出Bundle B的声明导出的包,即org.osgi.service.http。
visibility参数的具体使用方法是:附加参数visibility仅用于Require-Bundle标记,描述来自被导入Bundle中的Package是否要在导入Bundle中重新导出。默认值为private,代表不会在导入Bundle中重新导出。如果值为reexport,则说明需要重新导出。
在元数据信息中可以同时使用Import-Package和Require-Bundle标记来获取Bundle所需的依赖包,但是如果某个依赖的Package同时在Import-Package列表和Require-Bundle的Bundle中存在,那么OSGi实现框架必须保证要以Import-Package列表中的包优先。
使用Require-Bundle有时候确实会获得一些便利,但从长远来看,依赖的粒度越小越好。依靠Require-Bundle来处理依赖关系并非一种好的开发实践,甚至可能会带来一些令人头痛的问题,我们将在2.4.2节中通过实际例子来介绍Require-Bundle的缺陷。