- 浏览: 1658940 次
- 性别:
文章分类
- 全部博客 (2929)
- 非技术 (18)
- Eclipse (11)
- JAVA (31)
- 正则表达式 (0)
- J2EE (4)
- DOS命令 (2)
- WEB前端 (52)
- JavaScript (69)
- 数据库 (8)
- 设计模式 (0)
- JFreechart (1)
- 操作系统 (1)
- 互联网 (10)
- EasyMock (1)
- jQuery (5)
- Struts2 (12)
- Spring (24)
- 浏览器 (16)
- OGNL (1)
- WebService (12)
- OSGi (14)
- 软件 (10)
- Tomcat (2)
- Ext (3)
- SiteMesh (2)
- 开源软件 (2)
- Hibernate (2)
- Quartz (6)
- iBatis (2)
最新评论
打造一个基于OSGi的Web Application
动机和目标
OSGi技术发展至今也有好几年了,然而除了在富客户端应用(以Eclipse为代表)和服务器应用(如大多数的应用服务器)方面大放光芒之外,在
Web
Application方面的应用和资料却少之又少。一方面,在OSGi规范中,对于Web应用方面的规划尚不成熟,即使在最新的4.2版中,也仅仅只有
一个HttpService,这个简陋的service甚至不能覆盖任何一个现有的Servlet规范;另一方面,各个OSGi实现厂商对
HttpService的实现也是不完全的,在开发实现一个常规的Web
Application时,这些实现也是完全不够用的。本文章的目的,也就是为了探索OSGi在Web
Application上的开发之路该如何走,从我的视角提出一些看法,做一些尝试,希望对大家有所帮助。
现在OSGi与Web Application的结合,大致有两个方向:
- OSGi包含Web Container:目前能完美嵌入OSGi的Web Container似乎只有jetty一个,tomcat的catalina似乎有希望能成为第二个。我们完全不能指望Websphere和 Weblogic能在短期内具有能嵌入OSGi的能力,所以这个方向理所当然的被我放弃了。
- Web Container包含OSGi:这个方面目前只有equinox的Servlet Bridge这么一个著名的实现,equinox通过Servlet Bridge的方式来实现一个OSGi的HttpService服务,这个服务目前能做的事情还非常有限,还不足以覆盖Servlet规范。
我的目标是构建一个OSGi与Web Application结合的方式,它要能满足一下几点需求:
- 基于OSGi的bundle和service。
- 适合绝大对数支持Servlet 2.4和Jsp 2.0规范的Web服务器。
- 适合现有的实现OSGi 4.2规范的OSGi Framework实现:equinox、felix和knopflerfish。
- 支持大部分Servlet 2.4和Jsp 2.0规范中声明的功能。
- 提供一个基于HttpService的服务实现,以此来兼容其他使用HttpService的service。
毫无疑问,我将采用Web Container中包含OSGi的方式来实现,具体的内容将在以后陆续提供。
搭建开发环境
工欲善其事必先利其器,在正式开发之前,花一点时间来构建开发环境还是有必要的。本章介绍一下我的开发环境。
我使用的开发环境如下:
- Eclipse:当然了,最新版3.52,其中包含了最新版的WTP(Eclipse Web Tools Platform),个人感觉,不比MyEclipse差,而且最重要的是,它是free的。
- equinox-SDK:版本为3.6M5,实现了OSGi R4 core framework specification 4.2。
- Tomcat:作为第一个实现的Web Container,我采用了Tomcat,从中抽取几个特定版本作为测试对象:5.5.28和6.0.26这两个版本,因为他们支持Java5和 Servlet2.4/Jsp2.0。
- JDK:当然Java5以上的,谁叫Equinox只支持Java5以上的呢,我采用的是jdk1.5.0.22。基于Websphere和 Weblogic的缓慢的JDK升级历程,我还是决定不采用Java6或者是7了。
以下是我的目录结构:
环境整合:
1.运行Eclipse,指定Workspace路径为:D:/dbstar/workspaces/OSGi
2.设置Plug-in Development的Target
Platform,增加equinox-SDK-3.6M5并设为默认,这样我们就可以使用equinox-SDK-3.6M5来作为我们开发
bundle的基准库,而不是使用Eclipse自带的plugin开发环境。
3.在Server配置中增加Tomcat两个版本的服务器。
自此,我的开发环境就已经设置好了,当然了,还有一些其他的个人习惯设置,比如说字体,默认编码设为UTF-8,Code
Template和Formatter等等,就不一一赘述了。
在下面一篇中,将介绍如何在Web Application中启动OSGi。
在WebApplication中启动 OSGi
本章将创建一个Web Application项目,并描述如何在此应用中启动OSGi。
首先,在Eclipse中创建一个Dynamic Web Project,名字为OSGi-Web,Context root为osgi。
这个项目只作为部署Web Application使用,相关java代码放在另外一个Java Project中,因此我们再创建一个新的Java
Project,名字为OSGi-Web-Launcher。然后在OSGi-Web项目的Java EE Module
Dependencies中设置OSGi-Web-Launcher为关联,这样在部署的时候,OSGi-Web-Launcher项目中的java代码
将为打包为jar存放到Web的WEB-INF/lib目录之中。
为了启动OSGi,我们在web中增加一个ServletContextListener监听器实现,并且通过这个监听器来控制OSGi容器的启动和终
止。
在OSGi-Web-Launcher项目中增加一个java类,类名为FrameworkConfigListener,实现接口
ServletContextListener,package为org.dbstar.osgi.web.launcher。在
contextInitialized方法中,增加启动OSGi的代码,在contextDestroyed方法中,增加停止OSGi的代码,这样我们就
可以使OSGi容器的生命周期与ServletContext的生命周期保持一致了。
启动OSGi容器:
感谢OSGi规范4.2给了我们一个简单统一的启动OSGi容器的方式,所有实现OSGi4.2规范的容器实力都应该实现这种启动方式,那就是通过
org.osgi.framework.launch.FrameworkFactory,同时,还必须在其实现jar中放置一个文件:META-
INF/services/org.osgi.framework.launch.FrameworkFactory,这个文件中设置了实际的
FrameworkFactory实现类的类名。在equinox-SDK-3.6M5的
org.eclipse.osgi_3.6.0.v20100128-1430.jar中,这个文件的内容
是:org.eclipse.osgi.launch.EquinoxFactory。
我们先写一个工具类来载入这个配置文件中的内容:
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7
8 public abstract class ServiceLoader{
9 public final static < E > Class < E > load(Class < E > clazz) throws IOException,ClassNotFoundException{
10 return load(clazz,Thread.currentThread().getContextClassLoader());
11 }
12
13 @SuppressWarnings( " unchecked " )
14 public final static < E > Class < E > load(Class < E > clazz,ClassLoaderclassLoader) throws IOException,
15 ClassNotFoundException{
16 Stringresource = " META-INF/services/ " + clazz.getName();
17 InputStreamin = classLoader.getResourceAsStream(resource);
18 if (in == null ) return null ;
19
20 try {
21 BufferedReaderreader = new BufferedReader( new InputStreamReader(in));
22 StringserviceClassName = reader.readLine();
23 return (Class < E > )classLoader.loadClass(serviceClassName);
24 } finally {
25 in.close();
26 }
27 }
28 }
然后获取到FrameworkFactory的实例类:
2 frameworkFactoryClass = ServiceLoader.load(FrameworkFactory. class );
3 } catch (Exceptione){
4 throw new IllegalArgumentException( " FrameworkFactoryserviceloaderror. " ,e);
5 }
6 if (frameworkFactoryClass == null ){
7 throw new IllegalArgumentException( " FrameworkFactoryservicenotfound. " );
8 }
实例化FrameworkFactory:
2 try {
3 frameworkFactory = frameworkFactoryClass.newInstance();
4 } catch (Exceptione){
5 throw new IllegalArgumentException( " FrameworkFactoryinstantiationerror. " ,e);
6 }
获取Framework的启动配置:
2 try {
3 // 载入Framework启动配置
4 configuration = loadFrameworkConfig(event.getServletContext());
5 if (logger.isInfoEnabled()){
6 logger.info( " LoadFrameworkconfiguration:[ " );
7 for (Objectkey:configuration.keySet()){
8 logger.info( " /t " + key + " = " + configuration.get(key));
9 }
10 logger.info( " ] " );
11 }
12 } catch (Exceptione){
13 throw new IllegalArgumentException( " LoadFrameworkconfigurationerror. " ,e);
14 }
启动配置读取外部配置文件,可以在此配置文件中增加OSGi容器实现类相关的配置项,例如Equinox的osgi.console:
2 private static Map < Object,Object > loadFrameworkConfig(ServletContextcontext) throws MalformedURLException{
3 StringconfigLocation = context.getInitParameter(CONTEXT_PARAM_OSGI_CONFIG_LOCATION);
4 if (configLocation == null )configLocation = DEFAULT_OSGI_CONFIG_LOCATION;
5 else if ( ! configLocation.startsWith( " / " ))configLocation = " / " .concat(configLocation);
6
7 Propertiesconfig = new Properties();
8 try {
9 // 载入配置项
10 config.load(context.getResourceAsStream(configLocation));
11 if (logger.isInfoEnabled())logger.info( " LoadFrameworkconfigurationfrom: " + configLocation);
12 } catch (IOExceptione){
13 if (logger.isWarnEnabled())logger.warn( " LoadFrameworkconfigurationerrorfrom: " + configLocation,e);
14 }
15
16 StringstorageDirectory = config.getProperty(PROPERTY_FRAMEWORK_STORAGE,DEFAULT_OSGI_STORAGE_DIRECTORY);
17 // 检查storageDirectory合法性
18 if (storageDirectory.startsWith(WEB_ROOT)){
19 // 如果以WEB_ROOT常量字符串开头,那么相对于WEB_ROOT来定 位
20 storageDirectory = storageDirectory.substring(WEB_ROOT.length());
21 storageDirectory = context.getRealPath(storageDirectory);
22 } else {
23 // 如果是相对路径,那么相对于WEB_ROOT来定位
24 if ( ! new File(storageDirectory).isAbsolute()){
25 storageDirectory = context.getRealPath(storageDirectory);
26 }
27 }
28 storageDirectory = new File(storageDirectory).toURL().toExternalForm();
29 config.setProperty(PROPERTY_FRAMEWORK_STORAGE,storageDirectory);
30 if (logger.isInfoEnabled())logger.info( " UseFrameworkStorage: " + storageDirectory);
31
32 return config;
33 }
然后,就可以获取framework实例了,通过framework来初始化,启动和停止OSGi容器:
2 framework = frameworkFactory.newFramework(configuration);
3 framework.init();
4
5 // 初始化Framework环境
6 initFramework(framework,event);
7
8 // 启动Framework
9 framework.start();
10
11 succeed = true ;
12 } catch (BundleExceptione){
13 throw new OSGiStartException( " StartOSGiFrameworkerror! " ,e);
14 } catch (IOExceptione){
15 throw new OSGiStartException( " InitOSGiFrameworkerror " ,e);
16 }
在initFramework方法中,主要做两件事情,一是将当前的ServletContext作为一个service注册到OSGi容器中去:
2 Propertiesproperties = new Properties();
3 properties.setProperty( " ServerInfo " ,servletContext.getServerInfo());
4 properties.setProperty( " ServletContextName " ,servletContext.getServletContextName());
5 properties.setProperty( " MajorVersion " ,String.valueOf(servletContext.getMajorVersion()));
6 properties.setProperty( " MinorVersion " ,String.valueOf(servletContext.getMinorVersion()));
7 bundleContext.registerService(ServletContext. class .getName(),servletContext,properties);
8 }
第二件事就是:在第一次初始化容器时,加载并启动指定目录中的bundle:
2 private static void initFramework(Frameworkframework,ServletContextEventevent) throws IOException{
3 BundleContextbundleContext = framework.getBundleContext();
4 ServletContextservletContext = event.getServletContext();
5
6 // 将ServletContext注册为服务
7 registerContext(bundleContext,servletContext);
8
9 Filefile = bundleContext.getDataFile( " .init " );
10 if ( ! file.isFile()){ // 第一次初始化
11 if (logger.isInfoEnabled())logger.info( " InitFramework " );
12
13 StringpluginLocation = servletContext.getInitParameter(CONTEXT_PARAM_OSGI_PLUGINS_LOCATION);
14 if (pluginLocation == null )pluginLocation = DEFAULT_OSGI_PLUGINS_LOCATION;
15 else if ( ! pluginLocation.startsWith( " / " ))pluginLocation = " / " .concat(pluginLocation);
16
17 // 安装bundle
18 FilebundleRoot = new File(servletContext.getRealPath(pluginLocation));
19 if (bundleRoot.isDirectory()){
20 if (logger.isInfoEnabled())logger.info( " LoadFrameworkbundlesfrom: " + pluginLocation);
21
22 FilebundleFiles[] = bundleRoot.listFiles( new FilenameFilter(){
23 public boolean accept(Filedir,Stringname){
24 return name.endsWith( " .jar " );
25 }
26 });
27
28 if (bundleFiles != null && bundleFiles.length > 0 ){
29 for (FilebundleFile:bundleFiles){
30 try {
31 bundleContext.installBundle(bundleFile.toURL().toExternalForm());
32 if (logger.isInfoEnabled())logger.info( " Installbundlesuccess: " + bundleFile.getName());
33 } catch (Throwablee){
34 if (logger.isWarnEnabled())logger.warn( " Installbundleerror: " + bundleFile,e);
35 }
36 }
37 }
38
39 for (Bundlebundle:bundleContext.getBundles()){
40 if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED){
41 if (bundle.getHeaders().get(Constants.BUNDLE_ACTIVATOR) != null ){
42 try {
43 bundle.start(Bundle.START_ACTIVATION_POLICY);
44 if (logger.isInfoEnabled())logger.info( " Startbundle: " + bundle);
45 } catch (Throwablee){
46 if (logger.isWarnEnabled())logger.warn( " Startbundleerror: " + bundle,e);
47 }
48 }
49 }
50 }
51 }
52
53 new FileWriter(file).close();
54 if (logger.isInfoEnabled())logger.info( " Frameworkinited. " );
55 }
56 }
以上就是启动OSGi容器的过程,相比较而言,停止容器就简单多了:
2 if (framework != null ){
3 if (logger.isInfoEnabled())logger.info( " StoppingOSGiFramework " );
4
5 boolean succeed = false ;
6 try {
7 if (framework.getState() == Framework.ACTIVE)framework.stop();
8 framework.waitForStop( 0 );
9 framework = null ;
10
11 succeed = true ;
12 } catch (BundleExceptione){
13 throw new OSGiStopException( " StopOSGiFrameworkerror! " ,e);
14 } catch (InterruptedExceptione){
15 throw new OSGiStopException( " StopOSGiFrameworkerror! " ,e);
16 } finally {
17 if (logger.isInfoEnabled()){
18 if (succeed)logger.info( " OSGiFrameworkStopped! " );
19 else logger.info( " OSGiFrameworknotstop! " );
20 }
21 }
22 }
23 }
最后,还有一件事情,就是将FrameworkConfigListener配置到web.xml中:
2 < listener >
3 < listener-class > org.dbstar.osgi.web.launcher.FrameworkConfigListener </ listener-class >
4 </ listener >
让我们来测试一下吧,在Eclipse中新建一个Server:
另外,在OSGi-Web-Launcher项目的classpath中增加
org.eclipse.osgi_3.6.0.v20100128-1430.jar,并且在Java EE Module
Dependencies中勾选这个jar,这样可以保证这个jar最终部署到Web
Application的WEB-INF/lib目录下去。同样,还需要增加commons-logging.jar。
然后就可以启动这个Server查看效果了。
附上本文中提到的源
代码
。
为OSGi容器提供Web Application环境
本章叙述如何在OSGi容器中提供必要的Web Application环境,其中包括Servlet 2.4、Jsp
2.0和Commons-Logging相关的package,使得其他在OSGi容器中的bundle可以import。
为了在OSGi容器中提供export的package,一般有三种方式:
- 一个常规的bundle,自身包含必要的class,同时在Export-Package中声明。
- 一个Host为System Bundle的Fragment Bundle,同样也可以在Export-Package中声明导出的package,只要这个package中的class在System Bundle的ClassLoader中能load到。
- 通过启动Framework的配置项:org.osgi.framework.system.packages和 org.osgi.framework.system.packages.extra。OSGi 4.2规范中描述了这两个标准的配置项。在这两个配置项中描述的package都等同于在System Bundle中声明了export。
对于在Web Application中运行的OSGi容器,一些必要的环境是通过Web
Container提供的,我们最好不要,也不应该用自己的类来替换,这包括了j2ee相关的jar,如servlet和jsp相关的jar等等。在一些
WebServer的实现中,会自动屏蔽Web Application的classpath中的j2ee相关的jar。
除了j2ee相关的jar之外,还有一些使用非常普遍的jar,比如说Apache
commons一类,其中最常用的大概就是commons-lang.jar、commons-io.jar和commons-logging.jar
了,这些jar最好也有Web Container来提供,或者有必要的话,在Web
Application中提供,而不是在OSGi容器中提供,这涉及到一些JVM层次的单例类,或者希望能由Web
Application级别来统一实现和配置的环境,最常见的应用就是日志配置了。通过由Web
Application提供的commons-logging来给OSGi容器中的环境使用,而commons-logging通过何种方式来实现,不需
要让OSGi内部知道。
至于导出package到OSGi的方式中,是采用第二种还是第三种,主要区别在于:第三种方式是加载framework时指定的,在其后的生命周期中不
可更改,而第二种方式则更符合OSGi动态加载的特性。
我采用第二种方式来给OSGi容器增加环境支持,具体操作很简单,以Servlet为例,首先编写一个文本文件,名字为:MANIFEST.MF,内容如
下:
2 Bundle-ManifestVersion: 2
3 Bundle-Name:ServletExtensionFragment
4 Bundle-SymbolicName:javax.servlet_extension ; singleton:=true
5 Bundle-Version: 2.4.0
6 Fragment-Host:system.bundle ; extension:=framework
7 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
8 Export-Package:javax.servlet ; version="2.4.0",
9 javax.servlet.http ; version="2.4.0",
10 javax.servlet.resources ; version="2.4.0"
11 Bundle-Vendor:dbstar
注意其中关键的header属性,Fragment-Host: system.bundle; extension:=framework
这样写才能保证这个Fragment Bundle在各种OSGi Framework实现中都能兼容。
保存以后,将这个文件放置到一个名字为META-INF的目录中,然后用jar命令打包成一个jar即可(或者用winrar打包,记得选择压缩方式为
zip,在打包后将zip后缀名改成jar,我通常都是这么干的)。
Jsp的MANIFEST.MF:
2 Bundle-ManifestVersion: 2
3 Bundle-Name:JspExtensionFragment
4 Bundle-SymbolicName:javax.servlet.jsp_extension ; singleton:=true
5 Bundle-Version: 2.0.0
6 Bundle-Vendor:dbstar
7 Fragment-Host:system.bundle ; extension:=framework
8 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
9 Export-Package:javax.servlet.jsp ; version="2.0.0",
10 javax.servlet.jsp.el ; version="2.0.0",
11 javax.servlet.jsp.resources ; version="2.0.0",
12 javax.servlet.jsp.tagext ; version="2.0.0"
commons-logging的MANIFEST.MF
2 Bundle-ManifestVersion: 2
3 Bundle-Name:CommonsLoggingExtensionFragment
4 Bundle-SymbolicName:org.apache.commons.logging_extension ; singleton:=true
5 Bundle-Version: 1.1.1
6 Bundle-Vendor:dbstar
7 Fragment-Host:system.bundle ; extension:=framework
8 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
9 Export-Package:org.apache.commons.logging ; version="1.1.1",
10 org.apache.commons.logging.impl ; version="1.1.1"
因为我用的是commons-logging-1.1.1.jar,所以version写的是1.1.1,大家可以修改成自己所使用的jar的版本。
将上面生成的三个jar放到OSGi-Web项目的WEB-INF/osgi/plugins目录下面。还记得我在上一章创建的那个Tomcat
Server么,clean一次,新的jar会部署到Tomcat中去,然后就可以运行Server了。
至于为什么是clean而不是publish,区别在于clean会清除所有OSGi容器创建出来的文件,这样下次启动OSGi时就会做一个
install bundle的事情,而publish不会自动install新加进去的bundle。
如果你使用的是equinox,那么你可以在控制台中看到Syetem Bundle现在多了几个Fragments,查看一下Servlet
Bundle,会显示下列信息,表示servlet 2.4的package在OSGi容器中已经可用了:
javax.servlet_extension_2 .4.0 [ 2 ]
Id = 2 , Status = RESOLVEDDataRoot = D:/dbstar/workspaces/OSGi/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/OSGi-Web/WEB-INF/osgi/configuration/org.eclipse.osgi/bundles/ 2 /data
Noregisteredservices.
Noservicesinuse.
Exportedpackages
javax.servlet ; version="2.4.0"[exported]
javax.servlet.http ; version="2.4.0"[exported]
javax.servlet.resources ; version="2.4.0"[exported]
Noimportedpackages
Hostbundles
org.eclipse.osgi_3 .6.0 .v20100128- 1430 [ 0 ]
Nonamedclassspaces
Norequiredbundles
最后提供几个本章提到的bundle给大家下载,大家就不用自己再起生成一个了。
javax.servlet_extension_2.4.0.jar
javax.servlet.jsp_extension_2.0.0.jar
org.apache.commons.logging_extension_1.1.1.jar
系统不让传扩展名为.jar的文件,大家下载后把扩展名改改吧,阿门。
在OSGi容器中管理Web元素的实现思路
要在OSGi容器中动态的管理Web相关的元素,目前有两种方式。
一种是通过类似于bridge的方式,现在外层的Web
Application中配置一个对应的元素,然后通过对这个元素将外部的访问桥接到OSGi容器内部。在OSGi容器内部,会有一个总控的
service,通过这个service来提供Web元素的动态管理,以及对外界请求的响应和分发。Equinox的Servlet
Bridge就是采用的这种方式。
另一种方式就是通过使用Web Container提供的原生的API来实现Web元素的动态管理,将Web
Container的部分功能作为Service注册到OSGi中,然后在OSGi中通过这个Service来动态的管理Web元素。
两种方式各有利弊:
对于bridge方式来说,优点是与Web Container实现无关,在多种Web
Container中均能正常使用;缺点也很明显,无法使用Web Container提供的各种特性,并且相当于自己要实现一部分Web
Server的功能,不仅是重复造轮子,而且还是个不怎么好的轮子,能否满足企业级应用的需求,还只是个未知数。
对于使用原生API的方式来说,优点就是完全使用Web Server自身的机制,可以保证性能和功能需求;缺点同样也很明显,现在流行的Web
Server开源的并不多,而且就算是开源的,也不一定提供了可以动态管理Web元素的API,就算是有相应的API,也不一定适合OSGi环境,而且同
一个Web Server的不同版本,API也很有可能会变化,这直接导致了原生方式实现的多样性和不兼容性。
幸运的是,在Tomcat 5.5.x和6.0.x中都提供了这样的API,在以后的章节中,我会着重针对Tomcat
5.5.x、6.0.x版本以及对bridge方式这三个场景,来实现对Web元素的动态管理。
Servlet 2.4中描述了如下可以在web.xml中配置的元素,这也是我要实现动态管理的目标:
- Context Parameter
- Error Page
- Filter
- Filter Mapping
- Listener
- Security role
- Servlet
- Servlet Mapping
- Welcome Page
增加日志输出功能
到目前为止,我们的基于OSGi内核的Web
Application还没有任何的日志输出功能,本章将介绍如何在这个Web应用中配置和输出日志。
在前面的配置中,我们的应用中只含有commons-logging.jar,而OSGi容器之外的代码中,均是通过配置commons
logging的Log对象来输出日志的,在默认的配置下,系统将采用Jdk14Logger来作为输出日志的实现,这对我们来说是远远不够的。我们下一
步将配置更加常用的Log4j在作为我们的日志输出实现,通过以下几个步骤:
一、为Web Application配置Log4j:
1.在OSGi-Web项目的Java EE Module Dependencies中,增加对log4j.jar的依赖关系。
2.在WEB-INF/config目录中,增加一个log4j.properties文件,内容如下:
2 log4j.appender.stdout = org.apache.log4j.ConsoleAppender
3 log4j.appender.stdout.Target = System.out
4 log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
5 log4j.appender.stdout.layout.ConversionPattern =% d{ABSOLUTE} % 5p % c{ 1 }: % L - % m % n
6
7 #DefaultLogFileConfigurationForOSGi
8 log4j.appender.OSGiLog = org.apache.log4j.DailyRollingFileAppender
9 log4j.appender.OSGiLog.DatePattern = ' . ' yyyy - MM - dd
10 log4j.appender.OSGiLog.File = ${osgi.root} / logs / OSGi.log
11 log4j.appender.OSGiLog.layout = org.apache.log4j.PatternLayout
12 log4j.appender.OSGiLog.layout.ConversionPattern =% d[ % t] %- 5p % c - % m % n
13
14 log4j.rootLogger = info,stdout
15
16 log4j.logger.org.dbstar = debug,OSGiLog
17 log4j.logger.org.eclipse = debug,OSGiLog
3.采用Spring Web的Log4j配置方式,在web.xml中增加如下配置:
2 < context-param >
3 < param-name > webAppRootKey </ param-name >
4 < param-value > osgi.root </ param-value >
5 </ context-param >
6 < context-param >
7 < param-name > log4jConfigLocation </ param-name >
8 < param-value > /WEB-INF/config/log4j.properties </ param-value >
9 </ context-param >
10
11 <!-- Initlog4j -->
12 < listener >
13 < listener-class > org.springframework.web.util.Log4jConfigListener </ listener-class >
14 </ listener >
4.在OSGi-Web项目的Java EE Module Dependencies中,增加spring相关jar的依赖。
经过以上4个步骤,我们在Web Application中使用commons
logging输出的日志,都可以通过Log4j来显示了。但是作为OSGi容器内部来说,这还不够。OSGi规范中推荐使用
org.osgi.service.log包中的LogService和LogReaderService来管理和显示OSGi日志。为了能正常显示
OSGi容器内部的日志,我们还需要将LogService、LogReaderService和OSGi容器外部的Log4j结合起来才行,为了达到这
个目的,我们还需要做以下几个步骤:
1.为OSGi容器增加一个org.osgi.service.log的实现包。在equinox-SDK-3.6M5开发包中,这个实现jar
是:org.eclipse.equinox.log_1.2.100.v20100118.jar,当然,还需要
org.eclipse.osgi.services_3.2.100.v20100108.jar,都放置到OSGi-Web工程的WEB-
INT/osgi/plugins目录下面。
2.为OSGi容器增加Declarative Services支持。在equinox-SDK-3.6M5开发包中,包含了一个DS的实
现:org.eclipse.equinox.ds_1.2.0.v20100125.jar,将这个jar和一个依赖的
jar:org.eclipse.equinox.util_1.0.100.v20090520-1800.jar部署到OSGi容器中,就可以使用
DS服务了。同样也放到plugins目录下面去。
3.新增一个plugin工程,名字为:org.dbstar.osgi.log,我们使用DS方式来获取服务,相关源代码如下:
OSGI-INF/log.xml
2 < scr:component xmlns:scr ="http://www.osgi.org/xmlns/scr/v1.1.0" enabled ="true" name ="logListener" xsi:schemaLocation ="http://www.osgi.org/xmlns/scr/v1.1.0http://www.osgi.org/xmlns/scr/v1.1.0/scr.xsd" >
3 < implementation class ="org.dbstar.osgi.log.LogListenerImpl" />
4 < reference cardinality ="1..1" interface ="org.osgi.service.log.LogReaderService" name ="LogReaderService" policy ="static" />
5 </ scr:component >
LogListenerImpl.java
2
3 import org.apache.commons.logging.Log;
4 import org.apache.commons.logging.LogFactory;
5 import org.osgi.service.component.ComponentContext;
6 import org.osgi.service.log.LogEntry;
7 import org.osgi.service.log.LogListener;
8 import org.osgi.service.log.LogReaderService;
9 import org.osgi.service.log.LogService;
10
11 public class LogListenerImpl implements LogListener{
12 private static final Loglogger = LogFactory.getLog(LogListenerImpl. class );
13
14 protected void activate(ComponentContextcontext){
15 LogReaderServiceservice = (LogReaderService)context.locateService( " LogReaderService " );
16 service.addLogListener( this );
17 }
18
19 protected void deactivate(ComponentContextcontext){
20 LogReaderServiceservice = (LogReaderService)context.locateService( " LogReaderService " );
21 service.removeLogListener( this );
22 }
23
24 public void logged(LogEntryentry){
25 Stringmsg = getMessage(entry);
26
27 switch (entry.getLevel()){
28 case LogService.LOG_DEBUG:
29 if (logger.isDebugEnabled()){
30 if (entry.getException() == null ){
31 logger.debug(msg);
32 } else {
33 logger.debug(msg,entry.getException());
34 }
35 }
36 break ;
37 case LogService.LOG_INFO:
38 if (logger.isInfoEnabled()){
39 if (entry.getException() == null ){
40 logger.info(msg);
41 } else {
42 logger.info(msg,entry.getException());
43 }
44 }
45 break ;
46 case LogService.LOG_WARNING:
47 if (logger.isWarnEnabled()){
48 if (entry.getException() == null ){
49 logger.warn(msg);
50 } else {
51 logger.warn(msg,entry.getException());
52 }
53 }
54 break ;
55 case LogService.LOG_ERROR:
56 if (logger.isErrorEnabled()){
57 if (entry.getException() == null ){
58 logger.error(msg);
59 } else {
60 logger.error(msg,entry.getException());
61 }
62 }
63 break ;
64 }
65 }
66
67 private StringgetMessage(LogEntryentry){
68 StringBuildermsg = new StringBuilder();
69 if (entry.getBundle() != null )msg.append( " [bundle: " ).append(entry.getBundle()).append( " ] " );
70 if (entry.getServiceReference() != null )msg.append( " [service: " ).append(entry.getServiceReference())
71 .append( " ] " );
72 msg.append(entry.getMessage());
73 return msg.toString();
74 }
75 }
META-INF/MANIFEST.MF
2 Bundle-ManifestVersion: 2
3 Bundle-Name:LogBundle
4 Bundle-SymbolicName:org.dbstar.osgi.log
5 Bundle-Version: 1.0.0
6 Bundle-Vendor:dbstar
7 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
8 Service-Component:OSGI-INF/log.xml
9 Import-Package:org.apache.commons.logging ; version="1.0.4",
10 org.osgi.framework ; version="1.3.0",
11 org.osgi.service.component ; version="1.1.0",
12 org.osgi.service.log ; version="1.3.0"
好了,打包成bundle
jar然后也扔到plugins目录下面,然后clean一下server,启动,现在能看到多了许多日志输出,现在OSGi内部通过
LogService输出的日志也能由Log4j接管了。
最后总结一下,LogService和LogReaderService是OSGi规范中提倡的日志标准,在equinox内部实现中大量使用了这种日
志,而commons
logging是我们开发常规程序时所常用的日志方式。在你的bundle代码中,具体要采用哪一种日志方式,并没有强制的要求,大家可以根据各人喜好来
选用。
顺便提一句,LogService有些美中不足的是,不能像commons
logging那样,显示出日志具体是从哪个java类的第几行输出的,不知道各位大虾是否有人知道该如何解决呢,希望不吝赐教:)
设置初始化bundle的 StartLevel
后来发现其中在初始化时加载bundle的方式,还有一些美中不足。这种方式加载的bundle都具有相同的启动顺序,即bundle的初始化默认
start
level,在之前均没有做过特别的设置,所以默认值都是1,这样会导致所有的bundle的启动顺序无法控制,在某些希望特殊bundle优先加载的场
合(如日志功能,需要最先加载),我们希望能够在bundle初始化的时候就能指定特别的start
level,这样所有的bundle就能按照我们预设的启动顺序来加载了。下面就是我优化过的初始化代码,能够解决启动顺序问题。
工作原理是这样的,首先,在原来存放初始化bundle的目录,也就是OSGi-Web工程的/WEB-INF/osgi/plugins目录下面再增加
一个名为start的目录,在start目录下,再按照期望设置的start level来建立子目录,例如,期望设置start
level为1的bundle,放到plugins/start/1目录下面;期望设置start
level为2的bundle,放到plugins/start/2目录下面,以此类推。
代码方面,设置bundle的start level,需要使用StartLevel Service,可以通过下面代码获得:
2 ServiceReferenceslRef = bundleContext.getServiceReference(StartLevel. class .getName());
3 StartLevelsl = slRef == null ? null :(StartLevel)bundleContext.getService(slRef);
然后设置initial bundle start level:
2 Stringbsl = bundleContext.getProperty( " org.osgi.framework.startlevel.beginning " );
3 if (bsl != null && isInteger(bsl))sl.setInitialBundleStartLevel(Integer.parseInt(bsl));
这样所有新安装的bundle的初始化start
level都将被设置为和系统配置项:org.osgi.framework.startlevel.beginning相同的值,以确保所有默认安装的
bundle都能启动。
修改osgi.properties中关于org.osgi.framework.startlevel.beginning的配置项,我改成了5:
2 #LevelServiceSpecificationonpage 235 formoreinformation.
3 #
4 org.osgi.framework.startlevel.beginning = 5
增加一个方法,用于安装一个目录下所有的直属bundle,并且设置start level:
2 FilebundleFiles[] = bundleRoot.listFiles( new FilenameFilter(){
3 public boolean accept(Filedir,Stringname){
4 return name.endsWith( " .jar " );
5 }
6 });
7
8 if (bundleFiles != null && bundleFiles.length > 0 ){
9 for (FilebundleFile:bundleFiles){
10 try {
11 Bundlebundle = bundleContext.installBundle(bundleFile.toURL().toExternalForm());
12 if (sl != null && bsl > 0 )sl.setBundleStartLevel(bundle,bsl);
13 if (logger.isInfoEnabled())logger.info( " Installbundlesuccess: " + bundleFile.getName());
14 } catch (Throwablee){
15 if (logger.isWarnEnabled())logger.warn( " Installbundleerror: " + bundleFile,e);
16 }
17 }
18 }
19 }
最后,遍历start目录下的子目录来安装所有的bundle:
2 FileslRoot = new File(bundleRoot, " start " );
3 if (slRoot.isDirectory()){
4 FileslDirs[] = slRoot.listFiles( new FileFilter(){
5 public boolean accept(Filefile){
6 return file.isDirectory() && isInteger(file.getName());
7 }
8 });
9
10 for (FileslDir:slDirs){
11 installBundles(bundleContext,slDir,sl,Integer.parseInt(slDir.getName()));
12 }
13 }
14
15 // 安装直属目录下面的bundle
16 installBundles(bundleContext,bundleRoot,sl, 0 );
2 try {
3 Integer.parseInt(value);
4 return true ;
5 } catch (NumberFormatExceptione){
6 return false ;
7 }
8 }
最后,由于Declarative
Services的存在,稍微调整了一下启动策略,所有包含Service-Component的header定义的bundle,也调用start方法
来启动:
2 if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED){
3 if (bundle.getHeaders().get(Constants.BUNDLE_ACTIVATOR) != null || bundle.getHeaders().get( " Service-Component " ) != null ){
4 try {
5 bundle.start(Bundle.START_ACTIVATION_POLICY);
6 if (logger.isInfoEnabled())logger.info( " Startbundle: " + bundle);
7 } catch (Throwablee){
8 if (logger.isWarnEnabled())logger.warn( " Startbundleerror: " + bundle,e);
9 }
10 }
11 }
12 }
clean Server然后启动Server,我们可以看到初始化后的bundle已经被赋予了指定Start Level。
附上initFramework方法的完整代码,更多的代码请参加以前的帖子:
2 private static void initFramework(Frameworkframework,ServletContextEventevent) throws IOException{
3 BundleContextbundleContext = framework.getBundleContext();
4 ServletContextservletContext = event.getServletContext();
5
6 // 将ServletContext注册为服务
7 registerContext(bundleContext,servletContext);
8
9 Filefile = bundleContext.getDataFile( " .init " );
10 if ( ! file.isFile()){ // 第一次初始化
11 if (logger.isInfoEnabled())logger.info( " InitFramework " );
12
13 StringpluginLocation = servletContext.getInitParameter(CONTEXT_PARAM_OSGI_PLUGINS_LOCATION);
14 if (pluginLocation == null )pluginLocation = DEFAULT_OSGI_PLUGINS_LOCATION;
15 else if ( ! pluginLocation.startsWith( " / " ))pluginLocation = " / " .concat(pluginLocation);
16
17 // 安装bundle
18 FilebundleRoot = new File(servletContext.getRealPath(pluginLocation));
19 if (bundleRoot.isDirectory()){
20 if (logger.isInfoEnabled())logger.info( " LoadFrameworkbundlesfrom: " + pluginLocation);
21
22 // StartLevelService,用于设置bundle的 startlevel
23 ServiceReferenceslRef = bundleContext.getServiceReference(StartLevel. class .getName());
24 StartLevelsl = slRef == null ? null :(StartLevel)bundleContext.getService(slRef);
25 // 设置新bundle的初始startlevel为系统配置 项:org.osgi.framework.startlevel.beginning的值
26 Stringbsl = bundleContext.getProperty( " org.osgi.framework.startlevel.beginning " );
27 if (bsl != null && isInteger(bsl))sl.setInitialBundleStartLevel(Integer.parseInt(bsl));
28
29 // 安装bundle并设置相应的startlevel
30 FileslRoot = new File(bundleRoot, " start " );
31 if (slRoot.isDirectory()){
32 FileslDirs[] = slRoot.listFiles( new FileFilter(){
33 public boolean accept(Filefile){
34 return file.isDirectory() && isInteger(file.getName());
35 }
36 });
37
38 for (FileslDir:slDirs){
39 installBundles(bundleContext,slDir,sl,Integer.parseInt(slDir.getName()));
40 }
41 }
42
43 // 安装直属目录下面的bundle
44 installBundles(bundleContext,bundleRoot,sl, 0 );
45
46 for (Bundlebundle:bundleContext.getBundles()){
47 if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED){
48 if (bundle.getHeaders().get(Constants.BUNDLE_ACTIVATOR) != null || bundle.getHeaders().get( " Service-Component " ) != null ){
49 try {
50 bundle.start(Bundle.START_ACTIVATION_POLICY);
51 if (logger.isInfoEnabled())logger.info( " Startbundle: " + bundle);
52 } catch (Throwablee){
53 if (logger.isWarnEnabled())logger.warn( " Startbundleerror: " + bundle,e);
54 }
55 }
56 }
57 }
58
59 if (slRef != null )bundleContext.ungetService(slRef);
60 }
61
62 new FileWriter(file).close();
63 if (logger.isInfoEnabled())logger.info( " Frameworkinited. " );
64 }
65 }
使用Tomcat原生API来动态管理 Web元素:原理
Tomcat的org.apache.catalina.Context接口提供了动态管理注入到Catalina Web
Container中的Web元素的API。在基于OSGi的Web
Application中,可以利用这个接口来实现在OSGi容器中动态管理Web元素的目的。为了达到这个目的,我们还需要做一些额外的配置。请注意,
以下方法仅适用于Tomcat,并非通用的实现,而且只针对5.5.28版和6.0.24版的Tomcat做过简单的测试。
首先我们要做的事情,就是将Tomcat的org.apache.catalina.Context实现类作为Service注入到OSGi容器中去。在
OSGi-Web工程的WebContent/META-INF目录中,增加一个context.xml文件,内容如下:
2 < Context privileged ="true" />
这样我们就可以使用org.apache.catalina.ContainerServlet这个接口类了,通过它可以访问Catalina的内部功
能,它有Catalina被类加载器加载,而不是我们的WebApplication类加载器。它的
Setter方法在这个Servlet的新的实例被放进Service时被执行。
接下来我们写一个Servlet,这个Servlet将实现ContainerServlet接口,请注意它是怎么工作的:
2
3 import java.util.Properties;
4
5 import javax.servlet.ServletException;
6 import javax.servlet.UnavailableException;
7 import javax.servlet.http.HttpServlet;
8
9 import org.apache.catalina.ContainerServlet;
10 import org.apache.catalina.Context;
11 import org.apache.catalina.Wrapper;
12 import org.dbstar.osgi.web.launcher.FrameworkConfigListener;
13 import org.osgi.framework.BundleContext;
14 import org.osgi.framework.ServiceRegistration;
15 import org.osgi.framework.launch.Framework;
16
17 public final class TomcatContextServlet extends HttpServlet implements ContainerServlet{
18 private static final long serialVersionUID = - 3977062987005392657L ;
19
20 private Wrapperwrapper;
21 private Contextcontext;
22
23 private ServiceRegistrationregistration;
24
25 public WrappergetWrapper(){
26 return wrapper;
27 }
28
29 public void setWrapper(Wrapperwrapper){
30 this .wrapper = wrapper;
31 if (wrapper == null )context = null ;
32 else context = (Context)wrapper.getParent();
33 }
34
35 @Override
36 public void init() throws ServletException{
37 // EnsurethatourContainerServletpropertieshavebeenset
38 if ((wrapper == null ) || (context == null )) throw new UnavailableException( " Wrappernotset. " );
39
40 // EnsurethatFrameworkhavebeenset
41 Frameworkframework = FrameworkConfigListener.getFramework();
42 if (framework == null ) throw new UnavailableException( " Frameworknotset. " );
43
44 // 将context注册为服务
45 registration = registerContext(framework.getBundleContext(),context);
46 }
47
48 private static ServiceRegistrationregisterContext(BundleContextbundleContext,Contextcontext){
49 Propertiesproperties = new Properties();
50 properties.setProperty( " DisplayName " ,context.getDisplayName());
51 properties.setProperty( " ContextPath " ,context.getPath());
52 return bundleContext.registerService(Context. class .getName(),context,properties);
53 }
54
55 @Override
56 public void destroy(){
57 if (registration == null ) return ;
58
59 Frameworkframework = FrameworkConfigListener.getFramework();
60 if (framework == null ) return ;
61
62 if (framework.getState() == Framework.ACTIVE)registration.unregister();
63 registration = null ;
64 }
65 }
通过ContainerServlet接口提供的setWrapper方法,我们获得了一个Wrapper实例,这个实例对应于
TomcatContextServlet部署到Tomcat中的封装类,通过其getParent方法我们就可以获得Servlet所在的
Context了。
接下来在init方法中,我们将获得的Context实例,通过Framework注册到OSGi容器中去。在destroy方法中,注销Context
的注册,这样形成了一个完整的生命周期。
然后,将这个TomcatContextServlet部署到web.xml中去:
2 < servlet >
3 < servlet-name > TomcatContextServlet </ servlet-name >
4 < servlet-class > org.dbstar.osgi.web.launcher.tomcat.TomcatContextServlet </ servlet-class >
5 < load-on-startup > 1 </ load-on-startup >
6 </ servlet >
设置<load-on-startup>使这个Servlet在WebContainer初始化时加载,否则它将没有加载的机会,因为我们在
应用中不会直接使用到这个Servlet。
最后还有一件事情不要忘记了,我们需要将org.apache.catalina及其相关的package
export到OSGi容器中去,这样才能在OSGi容器中供给bundle来import。参照《打造一
个基于OSGi的Web Application——为OSGi容器提供Web Application环境
》一文中提到的方式,我们将catalina.jar作为extension Fragment的方式,引入到OSGi容器中去。
Catalina的MANIFEST.MF:
2 Bundle-ManifestVersion: 2
3 Bundle-Name:CatalinaExtensionFragment
4 Bundle-SymbolicName:org.apache.catalina_extension ; singleton:=true
5 Bundle-Version: 5.5.28
6 Bundle-Vendor:dbstar
7 Fragment-Host:system.bundle ; extension:=framework
8 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
9 Export-Package:org.apache.catalina , org.apache.catalina.authenticator ,
10 org.apache.catalina.connector , org.apache.catalina.core , org.apache.cat
11 alina.deploy , org.apache.catalina.loader , org.apache.catalina.mbeans , or
12 g.apache.catalina.realm , org.apache.catalina.security , org.apache.catal
13 ina.session , org.apache.catalina.startup , org.apache.catalina.users , org
14 .apache.catalina.util , org.apache.catalina.valves
在接下来的章节中,我会逐一描述如何在基于Tomcat的OSGi容器中,如何实现各种Web元素的动态管理,尽请期待哦:)
最后提供几个本章提到的bundle给大家下载,大家就不用自己再起生成一个了。
org.apache.catalina_extension_5.5.28.jar
相关推荐
9. Web 应用 bundle:Web 应用 bundle 是一个基于 OSGi 和 Spring 的 Web 应用程序,能够提供更加强大的灵活性和可靠性。 本文通过一个简单实例,介绍了如何利用 Spring-DM 开发基于 OSGi 和 Spring 架构的 Web ...
4. **Virgo Tomcat Server**: 是一个基于OSGi的服务器,支持Spring DM(Dynamic Module),是Spring OSGi应用的理想运行平台。 **二、环境配置** 1. **Eclipse Maven配置**: 需要在Eclipse中设置Maven的安装路径和...
dmServer,全称为Dynamic Modules Server,是一个完全模块化的Java服务器,其基于OSGi,专为运行企业级Java应用和Spring应用而设计。dmServer的模块化特性使得它能够提供更加灵活和可靠的部署环境,对于那些需要频繁...
在基于OSGi和Spring开发Web应用中,OSGi(Open Services Gateway Initiative)是一个开放标准,用于创建模块化Java应用程序。它允许开发者将应用程序分解为独立的模块,称为bundle,每个bundle都包含自己的类路径、...
本示例以“基于OSGI的Web开发例子”为主题,主要介绍了如何利用Equinox框架在OSGi环境中进行Web应用的开发,通过一个简单的“Hello World”项目来展示其工作原理。 首先,我们需要理解Equinox。Equinox是Eclipse...
综上所述,这个基于OSGi和Spring的Web应用示例展示了如何利用这两种技术的优点,构建一个模块化、可维护且易于扩展的应用。通过OSGi的模块化,我们可以更好地管理组件的生命周期,实现热部署;借助Spring的DI和AOP,...
通过以上步骤,我们可以构建一个基于OSGi和Spring的企业级Web应用。这种组合不仅能够提升应用的可维护性和可扩展性,还能极大地提高开发效率和系统的稳定性。 综上所述,将OSGi和Spring结合起来使用,可以有效地...
本文深入浅出地解释了如何结合Felix和Struts2构建基于OSGi的Web应用,通过一个具体的时间服务示例展示了OSGi的动态部署和模块化特性。这种开发方式使得Web应用的维护和扩展更为灵活,适应性强,尤其适合需要长期运行...
Web Application Bundle (WAB) 是一种特殊的OSGi Bundle,它可以作为一个Web应用运行。WAB包含传统的Web项目元素,如Web-INF目录和web.xml,同时还包含OSGi元数据,如MANIFEST.MF文件,使得它们能够作为OSGi模块运行...
### 开发一个简单的 OSGi Web 应用实例 #### 一、项目概述 本教程旨在通过一个具体的示例来介绍如何使用OSGi框架开发一个简单的Web应用。这个示例应用能够计算两个数字的和或乘积,并展示了OSGi bundle的动态部署...
Web示例工程是使用OSGi技术构建的一个具体应用,通常包括了如何在OSGi环境中部署和运行Web应用程序的实例。 在OSGi框架中,Equinox是Eclipse基金会提供的一个实现,它是OSGi规范的主要实现之一,广泛应用于服务器端...
Web Bundle允许开发者将一个传统的WAR(Web Application Archive)文件打包成OSGi Bundle,从而在OSGi环境中运行。这使得Web应用可以像普通OSGi服务一样被动态加载、卸载和更新,提高了系统的可维护性和可扩展性。 ...
总结而言,文章深入探讨了如何通过OSGi技术来构建一个更加灵活、高效的分布式Web应用结构,以应对大数据和高并发时代对Web应用系统性能和资源优化的需求。这种方法不仅优化了Web应用的设计,还降低了部署和运维成本...
基于 OSGi 和 Spring 开发 Web 应用
1、自己开发的OSGi Web Console,适用于实现了OSGi规范的Equinox、Felix等...2、展示了如何把OSGi框架作为一个组件嵌入到现有的未基于OSGi开发的Web应用当中,在Web应用中可获取OSGi中的Service以增加应用的灵活性。
**基于OSGi的Web应用开发**是现代软件开发中的一种技术实践,它允许开发者构建模块化、可扩展和可维护的Web应用。OSGi(Open Service Gateway Initiative)是一种开放的标准,提供了一种服务导向的、模块化的Java...
"基于 OSGi 的 RCP 测试 1 Equinox" 指的是一个关于使用 OSGi(Open Services Gateway Initiative)框架构建 Rich Client Platform (RCP) 应用程序的测试项目,重点是 Equinox 实现。Equinox 是 Eclipse 基金会的一...