提起模板引擎,大多数做J2EE的人第一反应是freemarker。毕竟freemarker出的时间早,使用的人也较多,常在互联网项目中使用。freemarker比起jsp来说不用编译,速度更快。但我们项目组的View层主要是Flex富客户端搞定,有模板引擎需求的时候选择了相对简单和轻量的velocity。下面回顾一下自己用velocity模板引擎写了一个代码生成器。
业务背景:
系统中共有千余张表。主要的申请信息、注册信息、流程信息约有400余张表。由于用户人为操作原因、录入/校对失误、系统BUG等难免会有错误数据出现。对错误数据的纠正之前总局内长期依赖于计算机处管理员用TOAD/PL SQL等工具去库里改,这种方式比较危险,且操作非常不便。所以新系统中需要一个可视化、全表信息均可修改的数据维护功能模块。
主要问题:
维护的表非常非常多,上面提到至少也需要维护数百张表。开发这样的模块若为没一张表开发页面则工作量非常巨大。和架构师商量,我们大体有两种思路:1、每一张表的页面动态生成,每次选择所需维护的表时,查该表所有字段,一一罗列出来即可。2、使用模板引擎,为每一张表生成一个相对合理布局的维护页面。经过讨论我们选择了后者,主要是前者动态生成的页面可能非常丑,样式、布局、拆分合并等操作在Flex的as中控制起来比较麻烦。
最终实现:
使用velocity模板引擎为对应的数百张表生成信息维护页面。代码包括展现部分(input、radio、checkbook、textarea、datagrid等元素组成)、事件注册、callback、对外接口等。废话不说,上代码:
核心方法—根据模板和参数使用VelocityEngine获取字符串:
/** * 根据模板取得传入参数后的字符串 * * @param tmpPath * 模板路径 * @param tmpFile * 模板名称,如果为空则抛出异常 * @param encoding * 取模板的字符集,如果为空则按照默认的utf-8 * @param names * 所有变量名称集合 * @param values * 所有变令对应的值 * @return 返回生成的对应的视图的字符串 * @throws Exception */ public static synchronized String getStringByTemplateFile(String tmpPath,String tmpFile, String encoding, String[] names, Object[] values)throws Exception { if (tmpFile == null || tmpFile.equals("")) throw new Exception("取得模板文件名称为空,无法生成对应的"); // 默认字符集为gbk if (encoding == null || encoding.equals("")) encoding = "gbk"; int len = 0;// 设置初始化变量个数 // 通过计算取names和values中个数少的为长度,有一个为null,则就不向里面设置变量,则得到就是视图模板 if (names != null && values != null) len = names.length < values.length ? names.length : values.length; // 初始化环境变量 Properties p = new Properties(); p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, tmpPath);// 存放模板的地址 VelocityEngine ve = new VelocityEngine(); ve.init(p); // 根据模板生成对应的对象 Template template = ve.getTemplate(tmpFile, encoding); VelocityContext context = new VelocityContext(); for (int i = 0; i < len; i++) context.put(names[i], values[i]); StringWriter writer = new StringWriter(); template.merge(context, writer); return writer.toString(); }
然后用IO将此文件写到名为“维护类型_维护表对应POJO类名.mxml”中。
下面是一个velocity模板的代码(makeAppFormMXMLGroup.vm):
其中有一些简单的循环、分支。
<?xml version="1.0" encoding="utf-8"?> <dc:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:dc="http://digitalchina.dc.tm" name="数据维护-申请信息维护-$!{tab.tabComments}维护"> <!-- 创建人:liujian 创建时间:$!{info.createTime} *************** --> <!-- 页面布局 ******************************************** --> <dc:layout> <s:BasicLayout/> </dc:layout> <!-- metada ****************元数据********************** --> <!-- script ******************AS脚本********************** --> <fx:Script> <![CDATA[ import $!{info.modePackageAs}.$!{tab.clsName}; import com.dc.business.tmaas.sjwh.components.SjwhUtils; [Bindable] public var data_$!{tab.clsName}:$!{tab.clsName} = null; //$!{tab.tabComments}对象,以"data_"+申请书对象命名 ##计算每一行的高度 #set($rowNum = $!{tcList.size()}/2) #if($!{tcList.size()}%2 != 0) #set($rowNum = $rowNum + 1) #end #set ($heightRate = 100/$rowNum) /** * 重置本页数据 * */ public function reset():void{ data_$!{tab.clsName} = new $!{tab.clsName}(); //重置表单样式,必须 SjwhUtils.resetTableStyle(mainTable); //页面有radio、dropdownList时,重置本页的radio、dropdownList } /** * 刷新本页数据方法,包含初始化下拉菜单等操作 * */ public function refresh():void{ //有dropdownList的场合,初始化dropdownList。 // if(data_TTmaasAppXXXXAppform.xxx == xxx){ // someDropDownList.selectedIndex = xxx; // } } /** * 申请信息变化监听 * */ protected function appInfoChangedHandler(labelName:String, event:Event):void{ if(data_$!{tab.clsName} != null){ parentDocument.conValueChangeHandler(event.currentTarget, data_$!{tab.clsName}, labelName); } } ]]> </fx:Script> <!-- declarations ********************声明非可视化组件************ --> <fx:Declarations> <!-- 将非可视元素(例如服务、值对象)放在此处 --> <mx:DateFormatter id="df" formatString="YYYY-MM-DD"/> </fx:Declarations> <!-- UICpmponents ****************可视化组件********************* --> <dc:Table width="100%" height="100%" id="mainTable"> <dc:Tr width="100%" height="$heightRate%"> ##设置一个变量,用于记录是奇数,偶数。 #set ($i=0) #foreach( $column in $tcList) #set($i=$i+1) <dc:Td horizontalAlign="right" verticalAlign="middle" styleName="tdColor" width="20%"> <dc:Label text="$!{column.colComments}" styleName="headerFont"/> </dc:Td> <dc:Td horizontalAlign="left" verticalAlign="middle" #if($column.dataType != 'DATE')paddingLeft="10"#end width="40%" #if($i == $!{tcList.size()} && $!{tcList.size()}%2 != 0)colSpan="3"#end> #if($column.dataType == 'DATE') <dc:DateField formatString="YYYY-MM-DD" text="{df.format(data_$!{tab.clsName}.$!{column.fieldName})}" name="$!{column.fieldName}" width="100%" height="100%" change="appInfoChangedHandler('$!{column.colComments}',event)"/> #else <dc:TextArea name="$!{column.fieldName}" text="{data_$!{tab.clsName}.$!{column.fieldName}}" #if($!{column.fieldName}== 'id' || $!{column.fieldName}== 'barCode' || $!{column.fieldName}== 'appNum') editable="false" #end borderVisible="false" width="99%" height="99%" focusOut="appInfoChangedHandler('$!{column.colComments}',event)"/> #end </dc:Td> ##偶数行/末尾行,关闭TR。 #if($i % 2 == 0 || $i == $!{tcList.size()}) </dc:Tr> #end ##偶数且非末尾行,开启新TR #if($i%2 == 0 && $i != $!{tcList.size()}) <dc:Tr width="100%" height="$heightRate%"> #end #end </dc:Table> </dc:Group>
下面是生成一些文件的结果:
然后使用一个NavigatorContent放到ViewStack中,creationPolicy="auto"自动创建。(这样在选择一个表的时候才创建页面对象,否则设置成all,数百个页面创建会内纯溢出。)
点击时,创建维护对象:
后来,经过引申,开发了一个功能更强大的代码生成器:
使用velocity模板生成hibernate的POJO、mapping文件,前台actionscript的model等等:
之后,当库结构发生改变时,用此页面重新生成POJO和mapping文件,打包即可。