大型CMS产品研发心得:参考OSGI实现插件机制

LeLeMei 2012-10-24

最近一两年一直在负责一个内容管理产品的研发,十几个人的团队,产品核心部分有40万行代码的规模,扩展功能有约10万行代码,分成50多个插件。说是大型CMS应用,实际上对于互联网应用来说依然是一个小型应用,毕竟产品基本不上不可能会运行在超过十台服务器的集群上。

首先说说我们的现状:

1、专职开发团队,有专职的设计工程师、前端工程师、JAVA工程师、测试工程师,本人担当产品经理,并负责一部分关键代码。

2、是起步阶段的公司,刚入职的同事技术水平都比较一般。

3、项目一般情况下都不会特别小,做不到只卖产品不需实施,客户一般都会提一些客户化的要求。但也做不到很大,一般不超过50万。

4、产品有过1.x版本,也积累了一些客户,但在项目实施上存在着较多问题。

这些问题是:

1、功能和功能之间藕合紧密,修改一处代码可能会影响多个功能,测试量很大。

2、藕合太紧导致可扩展性比较差,模块和模块之间边界比较模糊,有时候该使用接口却没有使用接口。

3、如果有合作伙伴或客户自己修改代码,又或者项目结束一段时间后(超过一年),就很难再和主版本同步了。

4、基本上需要为每个有客户化代码的项目保留一个SVN分支。

5、如果熟悉某一分支的同事离职,则后人接手维护需要有一个比较长的熟悉过程。

6、所有实施工程师都得熟知各个小版本之间的差异,以及每个小版本修正过什么BUG。

7、项目金额不大不小,既不可能让一个人只维护一个项目,又不可能完全不维护,导致每个人都得从SVN中检出多个分支版本。

总之,因为设计时对可扩展性考虑不足,导致了实施中的很多问题。因此我们想要引入插件机制,将功能分成一个个插件,插件之间有确定的依赖关系,如果不声明依赖哪个插件,则不允许引用该插件中的类,插件和插件之间以接口为契约。

OSGI作为极有声望的JAVA的组件技术,久经考验非常成熟,并且其开发有Eclipse做支撑,可以直接使用Eclipse开发OSGIbundle,所以是我们重点考察的对象。其优点不多说了,总之非常优秀。但我们发现有几个方面不是很符合我们这种类型的公司:

1、我们不能控制客户使用什么中间件、操作系统和数据库,因此我们的插件机制必须是在一个Webapp内部运行的,不可能将一个产品分成很多个war,让每个war就是一个bundle。

2、让OSGI在Webapp内部运行,感觉目前还不是很成熟,还不太适合企业应用开发,Equinox主页上有一篇文章讲到怎么在一个让OSGI在一个webapp内部运行,但自2007年后就没有更新过了,似乎没有投入什么力量来发展这一块。

3、使用OSGI基本上就只能让一个bundle成为一个工程,这样随着产品和公司的发展,一个workspace中会有上百个工程,再考虑分支版本和多个产品的情况,感觉很难加以整体掌握。

4、对于RCP或者Eclipseplug-in,开发和调试bundle都非常方便,修改后立即编译然后立即生效。但如果是WebApp,基本上不太可能达到这种效果,每次修改后都必须部署后才能运行和调试,开发效率会受到很大的影响。

5、OSGI本身未不支持PDE中的plugin.xml,非常重要的extensionpoint和extension相关的功能需要另行引入。

6、OSGI的学习曲线比较高,让每个产品开发人员达到能够定义extendsionpoint的水平相当困难,不适合我们目前的人力资源状况。

其实还有一个非常重要的问题是各种流行框架对OSGI的支持目前还有一些问题,使用Spring的同学可以考虑DM,能够解决一部分问题,但据说实际使用中也比较郁闷。我们使用的是自己开发的全套框架,不使用Servlet和JSP页面,只需要改造一下ClassLoader机制和资源文件、配置文件查找机制,就可以很快适应OSGI,所以不存在这个问题。

鉴于我们的技术水平、公司现状,均衡考虑开发效率和学习曲线,最终我们没有选择OSGI,而是决定参考了部分OSGI的设计,实现一个自己插件运行机制。主要有以下设计目的:

1、一个Eclipse工程中可以有多个插件,工程的组织方式和普通的J2EE工程完全相同,可以做到在Eclipse中即时修改即时编译即时生效,不需要在改动后执行部署动作,也不需要重启中间件。

2、插件可以单独安装、启动、停止、卸载,并且不需要重启中间件或应用。

3、插件可以单独在线升级,升级不需要重启应用。

4、插件如果被停止则依赖于该插件会自动停止,如果卸载了,则依赖于它的插件不能再启动。

5、每个插件有自己配置文件,配置文件以${PluginID}.plugin.xml命名,存放在一个专门目录里。

6、每个plugin.xml文件管理着自己对其他插件的依赖关系、菜单、扩展服务、扩展项以及哪些文件属于本插件。

7、系统中的菜单由各个插件通过plugin.xml提供,如果插件被停止,则相应的菜单会自动消失。

8、一个文件不可能同时属于两个插件。

9、插件只能引用被自己依赖的插件中的类(但没有实现OSGI中的Exportpackage和Importpackage功能)

10、允许第三jar在不同插件中有不同的版本(但不允许一个插件有多个不同版本)

11、以一种简化的方式实现了类似于PDE中extensionpoint和extension的功能。

12、普通开发人员必须能够在三天之内掌握插件开发机制。

其中比较特别的是扩展服务和扩展项:某一插件实现的功能需要依赖于本插件定义的某一接口的子类的集合时,需要定义扩展服务。扩展服务是一个插件配置项,用于声明本插件的一个扩展项注册入口;扩展项也是一个插件配置项,用于声明向哪个扩展服务注册扩展项。扩展项指定的类必须实现扩展服务指定的接口。

例如:内容核心插件需要管理所有的模板标签,但内容核心插件不知道其他插件都实现了什么标签,所以需要提供一个模板标签扩展服务。其他插件则可以将自己实现的模板标签扩展项注册到此服务,从而通知内容核心在模板编译、执行、标签展示等环节使用此标签。

扩展服务和扩展项是PDE中extensionpoint和extension的粗糙替代物,和extensionpoint的xsd方式不同的是,扩展服务只是简单定义扩展项需要使用的接口,所以语义性的东西都由接口通过get函数返回。这主要是因为我们的人员包括我本人对xsd不熟练,更愿意写接口。

为了达到以上设计目的,我们修改了自己的框架,主要修改的地方有两处:

1、全新设计了应用自己的ClassLoader,WEB-INF/lib下基本上没有jar包,绝大部分的jar包包括第三方jar包都被部署到了WEB-INF/plugins目录下,这是为避免jar文件被中间件的ClassLoader加载后不能进行动态更新的问题。ClassLoader还在运行时检查了插件之间的依赖关系。

2、静态变量和定时任务需要特别设计,可能会导致插件卸载和更新时不能回收ClassLoader实例的问题,从而导致PermGen内存泄漏。

另外,为了更好进行项目实施我们加了一个比较ugly的机制,将所有的页面/css/js/图片等资源文件打包到了单独的jar中,应用目录下基本上只有一个WEB-INF了。通过一个特别优化的(主要是处理Last-Modified头让浏览器尽量使用缓存)Filter来处理资源URL。这样做的目的是为了更好地进行实施,我们的框架会优先读取应用下的资源文件,找不到才会从jar中读取,这样项目实施时只需要在相应目录下放入该项目特有的资源(一般是登录界面什么的)就可以实现界面的客户化。

为此,我们开发了一系列的Eclipse插件,用来支撑我们的这种工程组织方式,提供编译检查、插件打包等服务,并提供了类似于PDE的界面来编辑我们自己的plugin.xml文件。

整个过程从决定自己开发到最终产品发布,历时近十个月,目前产品开发和定制开发效率都比较理想,总体来说付出的劳动还是值的。

相关推荐