对 OSGI 的简单理解
就像 Java web 应用程序需要运行在 Tomcat,Weblogic 这样的容器中一样.程序员开发的 OSGI 程序包也需要运行在 OSGI 容器中.目前主流的 OSGI 容器包括:Apache Felix 以及 Eclipse Equinox.OSGI 程序包在 OSGI 中称作 Bundle. Bundle 的整个生命周期都交与 OSGI 容器进行管理.可以在不停止服务的情况下,对 Bundle 进行加载和卸载,实现热部署. Bundle 对于外部程序来说就是一个黑盒.他只是向 OSGI 容器中注册了供外部调用的服务接口,至于实现则对外部不可见.不同的 Bundle 之间的调用,也需要通过 OSGI 容器来实现.
Bundle 如何引入 jar
刚才说到 Bundle 是一个黑盒,他所有实现都包装到了自己这个 "盒子" 中.在开发 Bundle 时,避免不了引用一些比如 Spring,Apache commons 等开源包.在为 Bundle 打包时,可以将当前 Bundle 依赖 jar 与 Bundle 的源码都打包成一个包(all-in-one).这种打包结果就是打出的包过大,经常要几兆或者十几兆,这样当然我们是不可接受的.下面就介绍一种更优的做法.
Bundle 与 OSGI 容器的契约
___Bundle 可以在 MANIFEST.MF 配置文件中声明他要想运行起来所要的包以及这些包的版本 !!! 而 OSGI 容器在加载 Bundle 时会为 Bundle 提供 Bundle 所需要的包 !!!___在启动 OSGI 容器时,需要在 OSGI 配置文件中定义
org.osgi.framework.system.packages.extra
,属性.这个属性定义了 OSGI 容器能提供的包以及包的版本.OSGI 在加载 Bundle 时,会将他自己能提供的包以及版本与 Bundle 所需要的包以及版本列表进行匹配.如果匹配不成功则直接抛出异常:
Unable to execute command on bundle 248: Unresolved constraint in bundle
com.osgi.demo2 [248]: Unable to resolve 248.0: missing requirement [248.0] osgi
.wiring.package; (&(osgi.wiring.package=org.osgi.framework)(version>=1.8.0)(!(version>=2.0.0)))
也可能加载 Bundle 通过,但是运行 Bundle 时报
ClassNotFoundException
.这些异常都由于配置文件没配置造成的.理解了配置文件的配置方法,就能解决 60% 的异常.
Import-Package
在 Bundle 的 Import-Package 属性中通过以下格式配置:
<!--pom.xml-->
<Import-Package>
javax.servlet,
javax.servlet.http,
org.xml.sax.*,
org.springframework.beans.factory.xml;org.springframework.beans.factory.config;version=4.1.1.RELEASE,
org.springframework.util.*;version="[2.5,5.0]"
</Import-Package>
包与包之间通过逗号分隔
可以使用 * 这类的通配符,表示这个包下的所有包.如果不想使用通配符,则同一个包下的其他包彼此之间可以使用; 分隔.
如果需要指定包的版本则在包后面增加
;version="[最低版本,最高版本]"
.其中 [表示大于等于,] 表示小于等于,)表示小于.
org.osgi.framework.system.packages.extra
语法与 Impirt-Package 基本一致,只是
org.osgi.framework.system.packages.extra
不支持通配符.
错误的方式
org.springframework.beans.factory.*;version=4.1.1.RELEASE
正确的方式:
org.springframework.beans.factory.xml;org.springframework.beans.factory.config;version=4.1.1.RELEASE,
Class 文件加载
在我们平时开发中有些情况下加载一个 Class 会使用
this.getClassLoader().loadClass
.但是通过这种方法加载 Bundle 中所书写的类的 class 会失败,会报
ClassNotFoundException
.在 Bundle 需要使用下面的方式来替换
classLoader.loadClass
方法
public void start(BundleContext context) throws Exception {
Class classType = context.loadClass(name);
}
Bundle 中加载 Spring 配置文件时的问题
由于 Bundle 加载 Class 的特性, 会导致在加载 Spring 配置文件时报错.所以需要将 Spring 启动所需要的 ClassLoader 进行更改,使其调用
BundleContext.loadClass
来加载 Class.
String xmlPath = "";
ClassLoader classLoader = new ClassLoader(ClassUtils.getDefaultClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
return currentBundle.loadClass(name);
} catch (ClassNotFoundException e) {
return super.loadClass(name);
}
}
};
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.setBeanClassLoader(classLoader);
GenericApplicationContext ctx = new GenericApplicationContext(beanFactory);
ctx.setClassLoader(classLoader);
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader) {
@Override
public void setClassLoader(ClassLoader classLoader) {
if (this.getClassLoader() == null) {
super.setClassLoader(classLoader);
}
}
};
ctx.setResourceLoader(resourceLoader);
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx);
reader.loadBeanDefinitions(xmlPath);
ctx.refresh();
Web 应用集成 OSGI
这里选用了 Apache Felix 来开发,主要是因为 Apache Felix 是 Apache 的顶级项目.社区活跃,对 OSGI 功能支持比较完备,并且文档例子比较全面. 其实 OSGI 支持两种方式来部署 Bundle.
单独部署 OSGI 容器,通过 OSGI 自带的 Web 中间件(目前只有 jetty)来对外提供 Web 服务
将 OSGI 容器嵌入到 Web 应用中,然后就可以使用 Weblogic 等中间件来运行 Web 应用
从项目的整体考虑,我们选用了第二种方案.
BundleActivator 开发
开发 Bundle 时,首先需要开发一个 BundleActivator.OSGI 在加载 Bundle 时,首先调用 BundleActivator 的 start 方法,对 Bundle 进行初始化.在卸载 Bundle 时,会调用 stop 方法来对资源进行释放.
public void start(BundleContext context) throws Exception;
public void stop(BundleContext context) throws Exception;
在 start 方法中调用
context.registerService
来完成对外服务的注册.
Hashtable props = new Hashtable();
props.put("servlet-pattern", new String[]{"/login","/logout")
ServiceRegistration servlet = context.registerService(Servlet.class, new DispatcherServlet(), props);
context.registerService 方法的第一个参数表示服务的类型,由于我们提供的是 Web 请求服务,所以这里的服务类型是一个
javax.servlet.Servlet
,所以需要将
javax.servlet.Servlet
传入到方法中
第二个参数为服务处理类,这里配置了一个路由 Servlet,其后会有相应的程序来处理具体的请求.
第三个参数为 Bundle 对外提供服务的属性.在例子中,在 Hashtable 中定义了 Bundle 所支持的 servlet-pattern.OSGI 容器所在 Web 应用通过 Bundle 定义的 servlet-pattern 判断是否将客户请求分发到这个 Bundle.servlet-pattern 这个名称是随意起的,并不是 OSGI 框架要求的名称.
应用服务集成 OSGI 容器
首先工程需要添加如下依赖
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.framework</artifactId>
<version>5.6.10</version>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.http.bundle</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.http.bridge</artifactId>
<version>3.0.18</version>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.http.proxy</artifactId>
<version>3.0.0</version>
</dependency>
然后在 web.xml 中添加
<listener>
<listener-class>org.apache.felix.http.proxy.ProxyListener</listener-class>
</listener>
开发
ServletContextListener
用以初始化并启动 OSGI 容器 请参考 Apache Felix 提供的 例子程序 .例子中提供的 ProvisionActivator 会扫描 / WEB-INF/bundles/,加载其中的 Bundle 包.(当然例子中提供的 ProvisionActivator 并不带有 Bundle 自动发现注册等机制,这些逻辑需要自行增加.请参照后续的 Bundle 自动加载章节)
路由开发
通过上面的配置,只是将 OSGI 容器加载到了 Web 应用中.还需要修改 Web 应用程序路由的代码.
在 Bundle 加载到 OSGI 容器中后,可以通过
bundleContext.getBundles()
方法获取到 OSGI 容器中的所有已经加载的 Bundle.
可以调用 Bundle 的
bundle.getRegisteredServices()
方法获取到该 Bundle 对外提供的所有服务服务.
getRegisteredServices
方法返回 ServiceReference 的数组.前文中我们调用
context.registerService(Servlet.class, new DispatcherServlet(), props)
我们已经注册了一个服务,
getRegisteredServices
返回的数据只有一个 ServiceReference 对象.
获取 Bundle 所能提供的服务 可以通过 ServiceReference 对象的 getProperty 方法获取
context.registerService
中传入的 props 中的值.这样我们就能通过调用
ServiceReference.getProperty
方法获取到该 Bundle 所能提供的服务.
通过上面提供的接口,我们可以将 Bundle 对应 ServiceReference 以及 Bundle 对应的 servlet-pattern 进行缓存.当用户请求进入到应用服务器后,通过缓存的 servlet-pattern 可以判断 Bundle 是否能提供用户所请求的服务,如果可以提供通过下面的方式,来调用 Bundle 所提供的服务.
ServiceReference sr = cache.get(bundleName);
HttpServlet servlet = (HttpServlet) this.bundleContext.getService(sr);
servlet.service(request, response);
Bundle 自动加载
在 Apache Felix 例子中提供的 ProvisionActivator, 只会在系统启动时加载 / WEB-INF/bundles / 目录下的 Bundle.当文件夹下的 Bundle 文件有更新时,并不会自动更新 OSGI 容器中的 Bundle.所以 Bundle 自动加载的逻辑,需要我们自己增加.下面提供实现的思路:
在第一次加载文件夹下的 Bundle 时,记录 Bundle 包所对应的最后的更新时间.
在程序中创建一个独立线程,用以扫描 / WEB-INF/bundles / 目录,逐个的比较 Bundle 的更新时间.如果与内存中的不相符合,则从 OSGI 中获取 Bundle 对象然后调用其 stop 以及 uninstall 方法,将其从 OSGI 容器中卸载.
卸载后,再调用
bundleContext.installBundle
以及 bundle.start 将最新的 Bundle 加载到 OSGI 容器中
BundleListener
最后一个问题,通过上面的方式,可以实现 Bundle 的自动加载.但是刚才我们介绍了,在路由程序中,我们会缓存 OSGI 容器中所有的 Bundle 所对应的 ServiceReference 以及所有 Bundle 所对应的 servlet-pattern.所以 Bundle 自动更新后,我们还需要将路由程序中的缓存同步的进行更新. 可以通过向 bundleContext 中注册 BundleListener,当 OSGI 容器中的 Bundle 状态更新后,会调用 BundleListener 的 bundleChanged 回调方法.然后我们可以在 bundleChanged 回调方法中书写更新路由缓存的逻辑
this.bundleContext.addBundleListener(new BundleListener() {
@Override
public void bundleChanged(BundleEvent event) {
if (event.getType() == BundleEvent.STARTED) {
initBundle(event.getBundle());
} else if (event.getType() == BundleEvent.UNINSTALLED) {
String name = event.getBundle().getSymbolicName();
indexes.remove(name);
}
}
});
来源: http://www.bubuko.com/infodetail-2470056.html