问题:公司的一个遗留系统具有运行期修改domain字段定义的特性,背后的实现原理是数据库中有"Table"表,“Field”表以及对应的实体表等,需要修改domain字段定义的时候程序会把信息存入Table表和Field表,同时执行alter语句动态修改对应的实体表结构,并存入对应数据。而程序代码中并不含对应的java class,数据都是在运行期“组装”起来以XML格式输出。这种设计看起来似乎没什么问题,但在实际的维护中很让人不爽。经常出现的情况是,美国同事(非软件开发从业者)发邮件来要求某个页面添加一些字段,我收到邮件后以管理员身份登录系统,执行一系列繁琐的操作...往往要消耗我数小时的宝贵时间。自从07年接手这个系统以来,我已经忍受了整整3年了!实在忍受不了了,决定重新开发,尽早脱离苦海。于是上次我回邮件说:
2周前正式开始开发新系统。最初的设想是,严格遵循DRY原则,数据模型的定义只存在于Domain Class中,采用0管理的面向对象数据库db4o - db4o内置了对domain class的重构支持。
可选的方案:
1. 每次需要更改domain class时,直接在IDE中修改,把编译后的classes上传到生产环境服务器,然后重新启动应用。
2. 利用JRebel,让它在运行期做到domain classes的热切换。
方案1的缺点是每次都得重启,已登录的用户需要重新登录,不太适合生产环境(尽管那个遗留系统有时也要重启让某些修改生效,尽管在用户保存数据的时候正好重启的几率很小,尽管。。。)
方案2让我们需要额外支付一笔钱购买JRebel的License,同时JRebel比较适合开发环境,生产环境下应该没人用吧?JRebel开发团队说,目前还没有开发适于生产环境的JRebel。就算JRebel在生产环境不影响应用的性能,我仍然需要执行编译+上传的额外操作,不能做到“分分钟搞掂”,心有不甘。当然可以在应用中植入自动编译引擎,省却手工编译的工作,但前提必须是“JRebel在生产环境下性能表现很好”。
作为完美主义者的我毫不犹豫抛弃了这两条方案。
......
Groovy的meta programming特性给了我一些启发。我设想:因为db4o是利用反射从我的domain classes中读取它的field信息的 - 相当于传统关系型数据库的“表结构”, 我能不能hack一把,写个客制化的反射器,让它从其它地方(比如配置文件或db4o数据库本身)读取field信息呢?我纵身投入Db4o的代码之海,在其中七十进七十出,寻得财宝若干。
基于设想+对db4o源代码的探索,我引入了dynabean( 不同于struts中的dynabean),以下就是一个dynabean:
package com.grs.sctms.dynabeans import com.grs.ast.GrsDomainClass @GrsDomainClass class MonitorVisitTrack {}
它里面没有任何field的信息,实际上系统中所有的dynabean都长得像这个样子,只是类名各异而已。
在这里还涉及到CoC原则:我并没有使用@dynabean标注,而是所有在com.grs.sctms.dynabeans命名空间中的domain被认为是dynabean.
那么它的fields信息在哪里呢?在这儿:
package com.grs.sctms.metadata import com.grs.ast.GrsDomainClass @GrsDomainClass class ClassMetadata { String cls String[] aspects // like ['String name [nullable:false, unique:true, validator:{v,o->v=~/grs\-cro\.com$/}]', 'Integer age'] static constraints = { cls nullable:false, unique:true, validator: {cls,meta->cls.startsWith('com.grs.sctms.dynabeans.')} } }
这样我如果要新建或修改domain class MonitorVisitTrack的fields信息,就不需要修改 MonitorVisitTrack本身了。在web-based console中执行如下代码,将会保存MonitorVisitTrack类的fields信息(四个字段):
new ClassMetadata( cls: 'com.grs.sctms.dynabeans.MonitorVisitTrack' ,aspects: [ 'List<Personnel> monitors [nullable:false,validator:{v,o->v.size()>0}]' ,'Date dateVisitStarted [nullable:false]' ,'Date dateVisitEnded [nullable:false]' ,'MonitorVisitType visitType [nullable:false]']).save()
无需重启,即刻生效。
String类型的定义,如
'Date dateVisitStarted [nullable:false]'
将会在运行期被解析成db4o的FieldMetadata对象...
我试着保存了几条MonitorVisitTrack数据,用UltraEdit打开db4o的数据文件,有如下发现:
1. 好消息:那四个字段的值(我赋予的是NULL)真的被持久化到数据库了(四个NULL在UltraEdit十六进制模式下依稀可辨)!
2. 坏消息:没有成功读取(OM中根本没有显示那四个字段)!
未完待续...