毕业设计的内容是Web应用程序的代码生成器,因为接触emf有一段时间了,我觉得用emf完全可以很方便的实现这个程序。这是更全面了解emf特别是codegen部分的一个好机会。这个帖子将记录这个过程的点滴,所以会比较琐碎,也许这些文字能被用在毕业论文里(不希望论文里都是google来的东西)。我发现cnblogs的帖子被修改后在rss里会重新出现,所以订阅我的blog的读者可能要经常被“骚扰”了:P
为了方便起见,我给这个代码生成器起了一个名字叫做“StarGen”,完全是一个代号,不包含任何意义。而且很幸运,这个名字在sourceforge.net上没有被注册过,等有了时间我要申请一下,所以程序里的包名都先以net.sf.stargen开头了。(请不要和我抢注这个名字啊,否则光refactor的工作就不少了,也许我就不开源了。)
闲话少说,以下是在实现StarGen过程中对一些问题的思考:
1、(3-24)首先我从使用者的角度审视一下这个应用程序完成后应该具有的功能,StarGen的使用者其实就是Web应用的开发者,他通过StarGen可以定义一些元素,这些元素将被转换为代码。粗略想了一下,可以定义的元素包括Class、Attribute、Operation、Reference等等,很像是一张类图。每个类将被转换为Web应用的一个模块,包含增、删、改、查询、列表、查看这些功能,这些功能分别由一个或多个jsp页面和Struts的Action(目前考虑生成基于Struts和Hibernate的应用,因为实验室一直使用它们)。
那么是否有了类图就够了呢,这样只能实现简单的数据操作,基本没有什么业务逻辑在里面。为了能让生成的Web应用具有逻辑性,我初步为StarGen的输入除类图以外增加了一个流程图,流程图里定义的流程会在生成的代码里得到体现。这个图和工作流里的那种图还是要有所区别的,我以前开发过工作流程序,在流程引擎里都要处理条件、会签、回退等情况,实际上是非常复杂的。因此流程这部分还需要花时间考虑一下怎样简化比较合适。
2、(3-24)既然明确了StarGen的输入,一个是类图,另一个是暂时称作流程图的东西,那么使用者应该怎样提供这些输入呢?首先看一下类图方面,这里使用者至少要能够定义出应用里应该有哪些对象类型,它们的成员变量、方法等等,还有类与类之间的联系。如果说他(使用者)定义的是一个模型,那么我首先要有一个元模型,元模型好比模型的语言,在元模型里我要定义“类”、“成员变量”、“方法”等等这些概念。按照这个想法,我想到平时使用emf建模时用的“语言”不就是ecore吗,没错,ecore是一个元模型,和uml一样,都可以用来定义模型。那么我可不可以就让使用者直接用ecore建模呢,因为使用ecore建模的工具已经有不少了,最起码也有随emf一起来的“Sample Ecore Model Editor”,想图形化可以用EclipseUML或者gmf的ecore editor都行,这样我可以节约很多工作量。
3、(3-24)图1中步骤3和步骤5是StarGen的主要工作所在。就像emf里有genmodel一样,StarGen也需要有自己的genmodel,不妨暂时称作“StarModel”吧。使用者输入的两个model被合并为一个starmodel,在starmodel里包含了它们的全部信息,此外还有一些如包名、类名这样的信息,这些信息和Web应用的业务逻辑没有什么关系。生成starmodel的部分我将主要org.eclipse.emf.importer这个项目,它可以从包括ecore模型在内的几种模型里生成genmodel。
另一项重要工作就是从starmodel生成可以执行的代码,这里我将参考以org.eclipse.emf.codegen开头的四个项目,特别是Generator类,我会从它“开刀”看看有什么值得利用。另外org.eclipse.uml2.codegen开头的几个项目也值得一看,uml2的代码生成借用了很多emf的东西,例如uml2的genmodel就完全是从emf的genmodel继承来的,只是增加了一些新的元素,我可能也会采取这种方式而非从头开始定义一个和emf的genmodel类似的starmodel。BTW,我觉得这不是走捷径,这叫“Leverage”!
4、(3-27)在emf里,从ecore生成genmodel的主要工作是在EcoreImporter中完成的,这个类继承ModelImporter类。在模型导入向导ModelImporterWizard的performFinish()方法里会调用prepareGenModelAndEPackages()和saveGenModelAndEPackages()方法,前者负责构造genmodel实例,后者负责保存这个实例。
ModelImporter的prepareGenModelAndEPackages()方法的主要过程是这样的:计算这个genmodel引用到的其他genmodel,将这些genmodel连同自己都加到同一个Resource内,调用genmodel的initialize()方法,这个方法会导致genmodel里的GenPackage的initialize()方法被调用,再导致GenClassifier的initialize()方法被调用,如此类推,直到整个genmodel构造完毕。然后,通过调用traverseGenPackages()和adjustGenModel()对得到的genmodel做一些调整,主要是一些属性的赋值,traverseGenPackages()的作用还不太明白。最后,调用reconcile()方法将原来的genmodel里被修改过的值应用到新生成的genmodel上,否则用户在Genmodel Editor里所做的修改在reload一次后就丢失了。
ModelImporter的saveGenModelAndEPackages()则十分直白,它会判断是否需要新建project,以及根据需要reference的其他genmodel决定在project里增加对哪些project的依赖(一般情况下好像都是在已有project里建立genmodel的),然后对需要保存的resource调用Resource#save()即可。
对于StarGen来说,保存的那部分应该是一样的,主要的修改会在生成genmodel的prepareGenModelAndEPackages()方法里,因为StarGen的genmodel(StarModel)在emf的genmodel基础上增加了Process等类,所以很可能需要在StarPackage的initialize()里对它们进行处理,这通过覆盖GenPackage()的initialize()方法就可以实现。
5、(3-29)EMF Model的新建向导(File->New->Others->Eclipse Modeling Framework->EMF Model)对应的类是EMFModelWizard,整个向导由两部分构成,第一部分是前两页,第一页叫NewGenModelFileCreationPage,在这里用户指定.genmodel文件的名字和所在项目,第二页是一个ModelConverterDescriptorSelectionPage的子类,它搜索注册到Eclipse上的org.eclipse.emf.importer.modelImporterDescriptors扩展点提供一个ModelImporter列表,例如EcoreImporter、RoseImporter等等。注意,在ModelConverterDescriptorSelectionPage的getNextPage()方法里会决定使用哪个Wizard作为向导第二部分,同时会调用adjustModelConverterWizard()方法做一些赋值工作。
向导的第二部分就由Importer决定,对于Ecore的情况对应EcoreImporterWizard,它是ModelImporterWizard的子类,包含两个向导页ModelImporterDetailPage和ModelImporterPackagePage,分别让用户选择一个.ecore文件和选择要生成或引用的包。ModelImporterWizard的子类都要实现createModelConverter()方法,返回一个ModelConverter类的实例,例如return new EcoreImporter(),EcoreImporter是ModelImporter的子类,ModelImporter则是ModelConverter的子类。
向导负责收集必要的信息,具体的生成.genmodel文件的工作在ModelImporter里完成,主要的步骤就是前面说过的prepareGenModelAndEPackages()和saveGenModelAndEPackages()方法。
6、(3-30)为了实现StarGen区别于emf的功能(流程等等),我“继承”emf的genmodel模型(genmodel.ecore)构造了StarGen的模型(stargen.ecore),这个过程参考了uml2对codegen的实现。StarGen的genmodel如下图所示:
需要注意的是,图中很多元素都要继承两个类,一个是emf中的GenXXX,另一个可能是StarBase,StarBase是GenBase的子类。这时要把GenXXX放在继承列表的第一位,否则生成的代码会缺少一些方法如getName()和reconcil()而无法编译通过。另外一点要注意的就是,在genmodel.genmodel(由genmodel.ecore生成,位于org.eclipse.emf.codegen.ecore/model)中,GenModel的Creation Commands属性是false,继承的stargen.genmodel里也要做同样的设置,否则在生成的editor里会出现MissingResourceException。不过,这样设置以后editor里就没有New Child/Sibling菜单选项了。
7、(3-31)初步使用自己的Template生成代码。实际上emf从大约2.2.0开始支持了Dynamic Template,只要把genmodel的“Dynamic Templates”属性设置为true,并在适当的位置放置另外的Template即可按这个Template生成代码。不过,为了更大的灵活性以及我暂时没有想到的情况,我没有使用这种机制。在emf的GenClassImpl这个类里有很多getXXXEmitter()方法,它的返回值决定了genmodel里各个元素使用哪个模板,例如GenClassImpl的getFactoryClassEmitter()是这样写的:
可以看到emf生成Factory类是通过org.eclipse.emf.codegen.ecore.templates.model.FactoryClass这个(经过编译的)模板完成的,如果想使用自己的模板,只要在StarModelImpl类里覆盖这个方法(StarModelImpl是GenModelImpl的子类,参考前面stargen的模型图),把setMethod()的参数改为"net.sf.stargen.templates.FactoryClass"即可。
模板是一些.javajet文件,在一个具有jet nature的project里,你写的所有.XXXjet文件在保存时都会被编译为一个.java类,该类的generate()方法生成.XXX文件的内容。emf的那些.javajet文件的内容还是比较复杂的,需要花些时间看看怎么利用,其中最主要是Class.javajet,因为StarGen不会去生成.edit和.editor的代码。再接下来就是要构造与Web应用有关的Template了,例如Action.javajet或是Mapping.hbm.xmljet等等,工作量也许很大,但一些问题应该可以先简化处理。
8、(4-5)现在StarGen可以从.ecore文件生成.starmodel文件了,并且可以生成一个Factory和n个bean-like类(n=ecore里EClass的数量),我对emf的Class.javajet做了巨大的简化,代码从1800多行减为不到100行,汗,真难想象emf的Class.javajet当时是怎样写出来的。下一步的工作是生成struts的配置文件,这里有一个之前没有考虑到的困难,那就是在xml文件里怎样控制jmerge的行为,应该与在java类里有所不同,还是要学习一下。
9、(4-10)生成.jsp文件的时候遇到一些困难。一是jsp与jet的控制标签都是"<% %>",必须在jet模板里指定使用不同的标签,例如startTag="<$" endTag="$>";二是GenBaseImpl类提供了多个generate()方法,用于生成.java文件的那个generate()方法不能用于生成.jsp文件,因为输出的文件名的扩展名在方法里是写死为".java"的,要使用具有五个参数的那个generate()方法,并自己计算输出文件的全名。
10、(4-11)StarGen生成如下代码:
模型元素 |
生成的代码 |
StarModel |
web.xml, hibernate.properties |
StarPackage |
Factory, struts-config.xml, Resource.properties(4-17), IndexAction(4-15), index.jsp(4-15) |
StarClass | Bean-like Class/Enum(4-18), Helper(4-14新增), XXXAction, XXXForm, JSPs(List, Create, Edit), struts-config-xxx.xml, ApplicationResources.properties, xxx.hbm.xml, validation.xml(4-18) |
接下来要花几天的时间对模板内容进行调整,这样就可以初步得到可以运行的生成结果。(PS.昨天看到一个很像的代码生成工具Modelstry,也是生成基于struts的web应用,还利用了spring来管理hibernate。但它好像不是用jet生成的代码,不知道merge方面是否有问题。刚下载了试用版本,有时间研究一下,取长补短。)
11、(4-13)生成了可以列表和添加新记录的web应用程序。第三方类库,包括Struts和Hibernate用到的那些jar包和tld文件,则需要用户手动添加到项目。GenFeature有一个方法是getGetAccessor(),得到的是bean里的getter方法名字,但却没有getSetAccessor(),为什么呢,因为getter不一定是以"get"开头的,还有"is"开头的情况,所以在getGetAccessor()里做了处理,而需要setXXX方法的地方直接写set<%=genFeature.getAccessorName()%>即可。
12、(4-14)实现了简单类的编辑和删除,jsp和action。为每个ecore类新生成一个Helper类,功能类似emf生成的ItemProvider,目前主要提供getText()方法,用户可以修改这个方法显示需要的内容。缺省的这个类返回<%=genClass.getSafeUncapName()%>.<%=genClass.getLabelFeature().getGetAccessor()%>()。
13、测试用的ecore模型,十分简单。
14、(4-17)处理了jsp页面里用checkbox显示GenFeature#isBooleanType()返回true的类型,在StarFeature里增加了password布尔变量,此值为true的用<html:password>代替<html:text>。非String类型的GenFeature,如int型和boolean型,不需特殊处理即可正常使用,原因是在ActionForm里已经是这种类型,本来担心的在Action的postCreate()/postEdit()方法里需要parse的问题并不存在。
15、(4-18)对特殊的GenFeature(即不是普通String类型的属性)做如下处理:
GenFeature情况 | 处理方法 |
---|---|
transient | 在hibernate映射文件里去掉对这类属性的声明 |
volatile | 待处理 |
boolean | 在create.jsp/edit.jsp页面中以checkbox展示 |
contains | 在container的编辑和查看页面中增加一个创建child的link |
ListType | 在create.jsp/edit.jsp页面中以multiple的select展示,在hibernate映射文件里使用<set>代替<property> |
reference | 在hibernate映射文件里使用<many-to-one>代替<property> |
MapType | 这一版本暂时不作处理 |
Bidirectional | 在hibernate映射文件里设置
|
EnumType | 假设GenFeature名为location,在模型类里增加一个int型的locationValue属性,mapping里只对这个属性持久化,而getEnum()里通过locationValue计算得到Enum类型返回值。生成继承AbstractEnumerator的类,生成一个Resource文件(为了对Enumerator的Literals实现国际化),在具有Enum类型成员变量的GenClass生成的struts-config-xxx.xml里要引用这个Resource文件。在Create.jsp/Edit.jsp里用下拉框展示,View.jsp/List.jsp里显示Resource里对应的文字内容(而非直接显示Literal),ActionForm里与模型类相对应,要有locationValue属性。(4-19) |
notNull | 在hibernate映射文件里设置
|
其他非String类型 | 在validation.xml文件里增加相应约束 |
16、(4-18)GenFeature常用的方法:getTypeGenClass()得到与GenFeature关联的“对面”的GenClass;
17、(4-19)让生成的java代码能够拥有适当的import声明的方法:在jet文件前部增加类似下面的语句:
18、(4-19)实现了对Enum类型GenFeature的处理,但生成的代码并不完美,在JSP页面里有类似下面的语句(因为用struts的标签无法很方便的表示,所以用<option>代替了<html:option>,同时在<bean:message>里还包含了jsp代码):
19、(4-20)生成的列表支持排序了。下面要处理的是分页问题,会稍微麻烦一些。
20、(5-10)针对流程的设计:流程(Process)由活动(Activity)组成,每个活动可以等价为对数据库的一个操作,例如增加一个用户就是在用户表里增加一条记录;又因为使用了Hibernate,所以对数据库的操作就是对持久对象的操作,例如增加用户就是增加一个User对象。
为了简化实现,暂时不考虑在流程中引入条件。
每个活动应该具有以下属性:
活动间目标对象的传递问题:例如第一个活动创建了Product1,第二个活动的内容是编辑Product1的属性,在流程设计中,如何告知第二个活动Product1的ID号?另外,会不会存在有多个ID号需要告知的情况?现在的想法是在session里维护这个对象,Select操作为这个对象赋值,流程结束时对其清空。
21、(5-11)因为流程里没有分支结构,所以Begin和End这两个Activity似乎没有必要存在了。今天完成了StarFlowModel的原始编辑器,用它创建的流程模型可以通过“Load Resources...”载入数据模型,并建立两个模型之间的关联。同时删除了原来设计在数据模型里的Process和Activity这两个类型。
接下来要考虑具体的流程模板内容了。
22、(5-13)基本完成了Select和Modify这两个活动的模板,利用它们可以组成“修改产品信息”这样的简单流程。剩下的活动类型还有Create、Delete和Custom这几种,其中Custom将生成没有任何功能的代码框架,让用户自己实现任意功能。
另外,流程对应的模板也已经完成了。