对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); } } });