使用 Ruby 开发代码生成器

使用 Ruby 开发代码生成器

使用 Ruby 开发代码生成器

 

高 尚, 软件工程师
李 莉, 软件工程师

 

简介: 在软件开发工程中,源代码生成工具是提升生产效率的利器。在我的工作中,曾经使用过 XLST、FreeMaker 等多种技术开发源代码生成器,最终我还是选择了脚本语言 Ruby。Ruby 以其简洁有趣的语法,功能强大的类库支持,使得我们能以较低的成本开发源代码生成器,且易于使用和维护。本文中我们将从 Ruby 的特性和几个常用类库讲起,结合一个 Java EE 项目,展示如何使用 Ruby 开发源代码生成器。希望能和读者分享 Ruby 技术和代码生成技术方面的一些心得。

发布日期: 2012 年 7 月 20 日
级别: 初级
访问情况 : 6589 次浏览
评论: 0 (查看 | 添加评论 - 登录)

平均分 5 星 共 1 个评分 平均分 (1个评分)
为本文评分

 

Ruby 简介

简介

Ruby 是一门免费的、简单的、直观的、可扩展的、可移植的、解释的脚本语言,用于快速而简单的面向对象编程。类似于 Perl、Python,它支持处理文本文件和执行系统管理任务等很多特性。说 Ruby 是当今世界脚本语言里最活跃的语言之一,毫不过分。Ruby on Rails Web 应用开发框架的盛行,更是让无数用户对 Ruby 简单、易用、高效的特性着迷。

语言特性

和 C,Java 等传统编程语言相比,Ruby 语言主要有以下一些特性 :

1, 一切皆对象。和 Java 不同,Ruby 中没有 Primitive Type 和 Object Type 的区别,语言中只有 Object Type。如清单 1 所示。


清单 1. 一切皆对象
				
"gin joint".length() » 9 
"Rick".index("c") » 2 
 -1942.abs() » 1942 
 cd.play(aSong) 

2, 代码块与迭代器。Ruby 语言中最具特色的地方要数代码块和迭代器。在 Ruby 中代码块可以作为函数的参数传递,通常我们使用代码块实现回调函数,在回调函数的基础上实现形形色色的迭代器。Ruby 中迭代器的大量使用,使得语言中很少看到 while 这样的循环语句。如清单 2 所示。


清单 2. 代码块和迭代器
				
 1.upto(10){|i| puts i} 
 ('a'..'e').each {|char| print char } 

3,Ruby Style 语法。在 C 或者 Java 等语言中,要交换两个变量的值时,通常的做法是引入临时变量,并做三次赋值操作(temp = a; a = b; b = temp)。Ruby 中的一个特性并行赋值,使得交换两个变量的值变得简单而有趣(a, b = b, a)。在 Ruby 中,单行 if 语句或者 while 语句可以把 if 和 while 放在语句的后面,这一语法特性也十分有趣。如清单 3 所示。


清单 3. 趣味语法
				
 a, b = b, a 
 puts "a = #{a}" if fDebug 
 a *= 2 while a < 100 

4,功能强大的运算符。脚本语言区别于传统语言的一个特性就在于,脚本语言的运算符代表的语义往往更加丰富。清单 4 演示了算符 * 在数组对象上的使用,字符串对象与正则表达式的匹配。


清单 4. 功能强大的运算符
				
 [ 1, 2, 3 ] * 3  »  [1, 2, 3, 1, 2, 3, 1, 2, 3] 
"varchar(16)" =~ /char/ » 3 

5,丰富的内置类库和第三方类库。Ruby 标准库涵盖了文件处理、网络 IO、系统管理、Web 开发、XML 处理、DB 操作等类库,足以满足日常应用开发的需要。除此之外还有大量开源的第三方类库可供使用。本文中我们将使用 REXML 类库和 ERB 类库。前者用于解析 XML 文件,后者用于构造模板。


代码生成器

什么是代码生成器

简单来说,代码生成器是一个程序,它输出的是代码。一个最常见的代码生成器是编译器,源程序经过编译器的处理后,编译输出特定平台的目标代码,可能是汇编代码,也可能是机器代码,或者是独立机器的中间代码,比如 Java 字节码。

除了编译器这类重量级的代码生成器外,在日常的软件开发过程中,我们还可以自己开发代码生成器,用于辅助我们生成源代码。实际上代码生成器能做的事情非常多,比如可以生成 SQL、配置文件、源程序甚至是文档,任何在日常工作中反复出现的工作产品。不能去除的重复,我们都可以想办法,通过代码生成来避免重复。

显而易见,代码生成要优于手工编写,其输出的代码质量更高,风格上更趋近于一致,便于阅读和后续维护。对一个庞大的软件工程来说,使用代码生成器能显著提高的生产率,节省时间和人力成本。

开发源代码生成器

本文中,我们将结合 Java EE 项目开发代码生成器。通常在这类项目中,都使用分层的原则来组织代码。一般来说,Java EE 项目通常被分为三层,分别是展示层,服务层和数据访问层,代码组织方式如图 1 所示。我们的代码生成器负责生成上述三层所有的 Java 代码,甚至包含 JSP、 JavaScript 和 SQL。


图 1. JavaEE 项目的代码组织方式

代码生成器的输入基于数据模型设计的结果。在我们的实践中,我们直接基于数据库设计结果生成源代码。我们使用了 PowerDesigner 设计数据库模型,PowerDesigner 的输出是 PDM 文件,这是一个 XML 文件,我们把 PDM 当成 XML 文件进行处理,从中读取数据模型信息。

读取数据模型信息后,下一步的工作是开发源代码模板。模板的开发是源代码生成器的关键,模板有两类信息组成,模型无关信息和模型相关信息,模型无关信息就是固定的字符串,模型相关信息通过占位符的方式指定,最终将模型信息和模板结合起来,模板中模型相关的占位符号被替换为模型中的要素,从而得到最终的源代码输出。


REXML

REXML 是 Ruby 中最重要的的 XML 类库之一。REXML 使用纯 Ruby 代码开发,支持 XML 标准和 XPath 标准。REXML 是 Ruby 标准库的一部分,安装完 Ruby 后即可直接使用。

在我们的源代码生成器中,REXML 被用来读取 PDM 文件,从中得到我们需要的数据模型信息。简单起见,在 PDM 中创建以下一个表,如图 2 所示:


图 2. 一个简单的数据模型

使用文本方式打开 PDM 文件,可以看到 PDM 的文档是基于 XML 格式的。上述示例中的数据模型对应的定义如清单 5 所示。


清单 5. 数据模型的 XML 格式
				
 <o:Table Id="o6"> 
 <a:Name> 用户表 </a:Name> 
 <a:Code>USER</a:Code> 
 <c:Columns> 
 <o:Column Id="o7"> 
 <a:Name>ID</a:Name> 
 <a:Code>ID</a:Code> 
 <a:DataType>int</a:DataType> 
 </o:Column> 
 <o:Column Id="o8"> 
 <a:Name> 姓名 </a:Name> 
 <a:Code>NAME</a:Code> 
 <a:DataType>char(16)</a:DataType> 
 </o:Column> 
 <o:Column Id="o9"> 
 <a:Name> 身份证号 </a:Name> 
 <a:Code>IDCARD</a:Code> 
 <a:DataType>char(18)</a:DataType> 
 </o:Column> 
 <o:Column Id="o10"> 
 <a:Name> 地址 </a:Name> 
 <a:Code>ADDRESS</a:Code> 
 <a:DataType>char(64)</a:DataType> 
 </o:Column> 
 </c:Columns> 
 </o:Table> 

为了表示数据模型信息,使用 Ruby 定义一个类 TableDef,如清单 6 所示:


清单 6. 数据模型的 Ruby 定义
				
 class TableDef 
	 #tableCode 表的英文名称
	 #tableName 表的中文名称
	 #clumns 是 2 维数组,其中每个元素为 [columnCode, columnName, columnType, isPK] 
	 attr_accessor :tableCode, :tableName, :columns 
	 def initialize(tableCode, tableName, columns) 
		 @tableCode = tableCode 
		 @tableName = tableName 
		 @columns = columns 		
	 end 
 end 

下一步我们使用 REXML 读取 PDM 文件,将前面定义的用户表的模型信息从文件中读取出来。其中我们使用了 XPath 从 XML 文件中定位某一个表,得到表对应的 XML Node,之后再从这个 Node 中读取表定义信息和相应的列信息。在 Ruby 中使用 REXML 读取 XML 文件的代码如清单 7 所示:


清单 7. 使用 REXML 读取数据模型
				
 require 'REXML/document'
 require './TableDef.rb'

 class PDMReader 
	 def initialize(path, tableCode,tableName=nil) 
		 @path = path 
		 @tableCode = tableCode 
		 @tableName = tableName 
	 end 
	
	 def getTableDef() 
	       xpath = @tableCode ?"a:Code=\"#{@tableCode}\"" : "a:Name=\"#{@tableName}\""
		 PDMDoc = nil 		
		 File.open(@path){|f| 
		   PDMDoc = f.read 
		 } 		
		 XMLDocument = REXML::Document.new(PDMDoc) 	
		 tableElement = REXML::XPath.first(XMLDocument, "//o:Table[#{xpath}]") 
		 tableCode = tableElement.elements[1,'a:Code'].text  
		 tableName = tableElement.elements[1,'a:Name'].text 		
		 colsElement = tableElement.elements[1,'c:Columns'] 
		 columns =[] 		
		 colsElement.each_element do |node| 
			 code = node.elements[1,'a:Code'].text 
			 name = node.elements[1,'a:Name'].text 		
			 type = node.elements[1,'a:DataType'].text 			
			 columns.push([code, name, type]) 
		 end 
		 return TableDef.new(tableCode,tableName,columns) 
	 end 
 end 

除了上述字段名,字段类型外,还可以从 PDM 中读取一个字段是否为主键等信息,实现的细节较为繁琐,这里不再详述。此外,如果在你的工作中,底层数据库模型不是 PDM XML 格式的,你需要自己依据你的模型文件编写相应的解析程序,读取相应的模型设计信息。


ERB —— Ruby 模板

ERB 是 Ruby 中一种文本模板技术,类似于 JSP,ASP,PHP。Ruby 1.9 之后,ERB 是 Ruby 标准库的成员。在源代码生成器的过程中,模板技术非常重要,如果没有模板工具,直接使用编程语言来处理,最终的结果将十分糟糕。早期 Java Web 开发中,过多地使用 Servlet 处理 HTML 输出就是一个典型的例子,JSP 技术的引入,就是作为一种模板去解放难以维护的 Servlet。

和 JSP, ASP,PHP 模板技术一样,ERB 中使用 <% %> 在模板文件中嵌入 Ruby 代码,<% %> 中的 Ruby 代码用于指定代码生成中数据模型相关的信息,模板中的文本给出了模型无关的信息。<% %> 中用于给出一段 Ruby 代码, <%= %> 表示表达式求值的结果将被输出出来。

如果 ERB 模板的生成目标是 JSP 页面,由于 JSP 中也使用 <% %>,为了能够正确的生成 JSP,在 ERB 中,要使用 <%% %%>。

清单 8 给出了一个 JavaEE 项目中,一个 Java 接口的 ERB 模板。这个 Java 接口对应三层架构中的服务层,声明了增删改查(CRUD)方法。


清单 8. 一个 Java 接口的 ERB 模板
				
 /* 
 * 项目名称:<%=projectName%> 
 * 
 * 创建日期 : <%=date%> 
 * 
 * 1.0.0 版 (<%=bean.author%>)
 */ 
 package com.<%=projectPackage%>.<%=bean.package%>.service; 

 import java.util.List; 

 import com.<%=projectPackage%>.<%=bean.package%>.domain.<%=bean.domain%>Domain; 

 /** 
 * <%=bean.desc%>Service 
 * @author <%=bean.author%> 
 */ 
 public interface I<%=bean.domain%>Service { 
	
	 /** 
	 * 根据 <%=bean.desc%> 主键查询 <%=bean.desc%> 信息
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain  
		 依据 <%=bean.downcaseDomain()%>Domain 中的主键属性查询
	 * @return one <%=bean.domain%>Domain 
	 */ 
	 public <%=bean.domain%>Domain find<%=bean.domain%>ById(<%=bean.domain%> 
	 Domain <%=bean.downcaseDomain()%>Domain) throws BusinessException; 
	
	 /** 
	 * 根据指定条件查询 <%=bean.desc%> 信息
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain  依据 <%=bean.downcaseDomain()%> 
		 Domain 中的非空属性查询
	 * @return a list of <%=bean.domain%>Domain 
	 */ 
	 public List<<%=bean.domain%>Domain> 
	 find<%=bean.domain%>ByWhere(<%=bean.domain%> 
	 Domain <%=bean.downcaseDomain()%> 	 Domain) throws BusinessException; 

	 /** 
	 * 保存或者更新 <%=bean.desc%> 信息 , 如果被保存的 vo 的主键为空 , 插入 vo, 否则更新 vo. 
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain 被保存的 domain 
	 */ 
	 public void doSaveOrUpdate<%=bean.domain%> 
	 (<%=bean.domain%>Domain < 
	 %=bean.downcaseDomain()%>Domain) throws BusinessException; 
	
	 /** 
	 * 保存 <%=bean.desc%> 信息
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain 被保存的 domain 
	 * @return 带主键信息的 domain 
	 */ 
	 public <%=bean.domain%>Domain doInsert<%=bean.domain%> 
	 (<%=bean.domain%>Domain <%=bean.downcaseDomain()%>Domain) 
	 throws BusinessException; 
	 /** 
	 * 删除 <%=bean.desc%> 信息
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain 被删除的 domain 
	 */ 
	 public void doDelete<%=bean.domain%> 
	 (<%=bean.domain%>Domain < 
	 %=bean.downcaseDomain()%>Domain) throws BusinessException; 
	
	 /** 
	 * 更新 <%=bean.desc%> 信息
	 * 
	 * @param <%=bean.downcaseDomain()%>Domain 被更新的 domain 
	 */ 
	 public void doUpdate<%=bean.domain%> 
	 (<%=bean.domain%>Domain < 
	 %=bean.downcaseDomain()%>Domain) throws BusinessException; 
 } 

编写完模板之后,在 Ruby 中调用以下代码,即可生成目标源代码,如清单 9 所示。在清单 9 中,首先指定了生成源程序的磁盘文件路径,然后使用模板文件创建一个 ERB 对象,在最后一步中,将当前上下文中的所有 Ruby 对象通过 binding 方法传递到 ERB 对象中,在 ERB 对象上调用 result 方法,生成目标源代码。


清单 9. 调用 ERB 模板
				
	 template =" Service.java"
	 path = "/src/com/#{projectPath}/#{bean.package}/service"  
	 File.makedirs(path) unless FileTest.exist?(path) 
	 file = "#{path}/I#{bean.domain}Service.java"
	 f = File.new(file, "w") 
	 File.open( template ) { |fh| 
	   ERB = ERB.new( fh.read ) 
	   f.print ERB.result( binding )    
	 } 


一个生成器的例子

生成器的开发流程

在这一节,我们将详细说明如何为 JavaEE 项目开发源代码生成器。第一步,我们以底层的数据库设计文档 PDM 出发,解析 PDM 文件,读取数据库表定义信息。第二步,我们将表定义映射到 Java 对象,在数据库表和 Java 对象之间建立一个映射关系。第三步,我们开发各种 JavaEE 模板,包括领域对象(Domain),SQL,DAO,Service 实现,Struts Action 和 JSP 等,映射关系和模板相结合,得到最终的目标源代码。整体流程如图 3 所示。


图 3. 生成器的开发流程

在 REXML 一节,已经详细说明了如何读取 PDM 文件得到数据库表定义信息。在生成表定义和 Java 对象的映射关系时,我们可以有两种方法,一种是使用 REXML 的 API 构造 XML DOM,然后写入文件;另一种是使用模板技术。在我们的实践中,使用了模板技术。清单 10,给出了 XML 的 ERB 模板,清单 11 是我们使用之前定义的 User 表,调用模板后得到的结果。


清单 10. 映射关系的 XML 模板
				
 <?XML version="1.0" encoding="UTF-8"?> 

 <beans> 

	 <bean package="example" name="<%=tableDef.tableCode.downcase.capitalize%> 
	" table="<%=tableDef.tableCode%>" desc="<%=(tableDef.tableName)%> 
	" author="generator"> 
	 <% tableDef.columns.each do |c|%> 
		 <property field="<%=c[0].downcase%>" column="<%=c[0]%> 
		"  desc="<%=c[1]%>" type="<%=c[2].downcase%> 
		" <%if(c[3]==true)%>pk="true"<%end%>/> 
	 <% end%> 		
	 </bean> 

 </beans> 

在这个模板中, 我们定义了 Java 类的 package,Java 类的类名称,Java 领域对象的属性名等信息,这些信息可以在生成最终的 Java 代码之前,手工修改。比如,修改 package 信息和 author 信息等等。


清单 11. USER 的映射关系
				
 <?XML version="1.0" encoding="UTF-8"?> 

 <beans> 

	 <bean package="example" name="User" table="USER" desc="用户" author="generator"> 
	
		 <property field="id" column="ID"  desc="ID" type="int" pk="true"/> 
	
		 <property field="name" column="NAME"  desc="姓名" type="char(16)" /> 
	
		 <property field="idcard" column="IDCARD"  desc="身份证号" type="char(18)" /> 
	
		 <property field="address" column="ADDRESS"  desc="地址" type="char(64)" /> 
			
	 </bean> 

 </beans> 

至此我们知道, 使用上述步骤生成的类似清单 11 的 XML 文件,才是代码生成器的真正输入。稍后将详细介绍几个 JavaEE 项目中使用的源代码模板。

模板文件详解

我们知道代码生成器的输如是形入清单 11 的 XML 文件,为了表示这种数据结构,我们定义一个 Ruby 类 BeanDef,用它描述代码生成器的输入。BeanDef 的定义如清单 12 所示。


清单 12. 映射关系的 Ruby 定义
				
 class BeanDef 
	 # properties 是 2 维数组,其中每个元素为 [field, columnName, desc, type] 
	 attr_reader :package, :domain, :table, :desc, :author, :properties 
 end 

有了上述的 BeanDef 信息后,剩余的工作就是为 JavaEE 项目的各个组件编写模板。下面分别给就领域对象(Domain),SQL,DAO,Service 实现,Struts Action 和 JSP 的 ERB 模板进行说明。

领域对象类用于描述业务数据,它是一个简单的 Java Bean,类由各个属性,以及属性的 getter 和 setter 方法构成。在我们的 ERB 模板中,类的属性存放在 BeanDef 类的 properties 中,properties 是一个数组,在我们的模板中两次遍历这个数组,第一次生成各个属性,第二次生成所有属性的 getter 和 setter 方法。如清单 13 所示。


清单 13. 领域对象类模板
				
 /* 
 * 项目名称:<%=projectName%> 
 * 
 * 创建日期 : <%=date%> 
 * 
 * 1.0.0 版 (<%=bean.author%>)
 */ 
 package com.<%=projectPackage%>.<%=bean.package%>.domain; 
 <%if(bean.hasDateType())%>import java.util.Date;<%end%> 

 public class <%=bean.domain%>Domain  { 	
 <% bean.properties.each do |p|%>  
	 /** <%=p.desc%> */ 	
	 private <%=p.getJavaType()%> <%=p.field%>; 	
 <%end%> 	
 <% bean.properties.each do |p|%>  
	 /** 
	 * 获取 <%=(p.desc)%> 
	 */ 
	 public <%=p.getJavaType()%> get<%=p.upcaseField()%>(){ 
		 return this.<%=p.field%>; 
	 } 
	 /** 
	 * 设置 <%=(p.desc)%> 
	 */ 
	 public void set<%=p.upcaseField()%>(<%=p.getJavaType()%> <%=p.field%>){ 
		 this.<%=p.field%> = <%=p.field%>; 
	 } 
 <%end%> 
 } 

使用 USER 数据模型作为输入,调用上述模板,得到的输出如清单 14 所示。


清单 14. UserDomain.java
				
 /* 
 * 项目名称:XX 系统
 * 
 * 创建日期 : 2011-12-08 
 * 
 * 1.0.0 版 (gaoshang)
 */ 
 package com.test.example.domain; 


 public class UserDomain { 	
  
	 /** ID */ 	
	 private Integer id; 	
  
	 /** 姓名 */ 	
	 private String name; 	
  
	 /** 身份证号 */ 	
	 private String idcard; 	
  
	 /** 地址 */ 	
	 private String address; 	
	
  
	 /** 
	 * 获取 ID 
	 */ 
	 public Integer getId(){ 
		 return this.id; 
	 } 
	 /** 
	 * 设置 ID 
	 */ 
	 public void setId(Integer id){ 
		 this.id = id; 
	 } 
  
	 /** 
	 * 获取 姓名
	 */ 
	 public String getName(){ 
		 return this.name; 
	 } 
	 /** 
	 * 设置 姓名
	 */ 
	 public void setName(String name){ 
		 this.name = name; 
	 } 
  
	 /** 
	 * 获取 身份证号
	 */ 
	 public String getIdcard(){ 
		 return this.idcard; 
	 } 
	 /** 
	 * 设置 身份证号
	 */ 
	 public void setIdcard(String idcard){ 
		 this.idcard = idcard; 
	 } 
  
	 /** 
	 * 获取 地址
	 */ 
	 public String getAddress(){ 
		 return this.address; 
	 } 
	 /** 
	 * 设置 地址
	 */ 
	 public void setAddress(String address){ 
		 this.address = address; 
	 } 
 } 

在我们的实践中,底层的数据库访问技术使用了 IBatis 框架,清单 15 给出了对应的 SQL 模板。

在这个模板中,分别定义了 resultMap(Java Bean 属性和数据库表字段的映射关系 ),以及查询(select)、新增(insert)、删除(delete)和更新(update)对应的 SQL。在这个模板中,查询和删除时数据时,依据表字段是否是主键进行判断,组装了有所有主键字段构成的查询和删除条件。


清单 15. SQL 模板
				
 <?XML version="1.0" encoding="UTF-8"?> 
 <!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
	"http://ibatis.apache.org/dtd/sql-map-2.dtd"> 
 <sqlMap namespace="com.<%=projectPackage%>.< 
 %=bean.package%>.dao.<%=bean.domain%>Dao"> 

	 <typeAlias alias="<%=bean.domain%>Domain"
		 type="com.<%=projectPackage%>.<%=bean.package%> 
		 .domain.<%=bean.domain%>Domain" /> 

	 <resultMap class="<%=bean.domain%> 
	 Domain" id="<%=bean.domain%>Domain"> 
		 <% bean.properties.each() do |p|%> 
			 <result property="<%=p.field%>" column="<%=p.column%>" /> 
		 <% end %> 		
	 </resultMap> 
    
	 <select id="find<%=bean.domain%>ById" parameterClass="< 
	 %if(bean.hasMultiplePK())%><%=bean.domain%> 
	 Domain<%else%>string<%end%>" resultMap="< 
	 %=bean.domain%>Domain"> 
		 select <% bean.properties.each_index() do |i|%>< 
		 %if(i > 0) %> ,<%end %> 
		 <%=bean.properties[i].column%>< 
		 % end %> <% isbegin = false; where ="" %> 	
		 from <%=bean.table%> where < 
		 % bean.properties.each() do |p|%><%if(p.isPK()) %>< 
		 %if(isbegin)%><% where<<" and "; end%>< 
		 % where<<p.column<<"=#"<<p.field<< 
		"#"  %><% isbegin =true end %><% end %> 
		 <%=where%> 	
	 </select> 

    <select id="find<%=bean.domain%>ByWhere" parameterClass="< 
    %=bean.domain%>Domain" resultMap="<%=bean.domain%>Domain"> 
		 select  <% bean.properties.each_index() do |i|%> 
		 <%if(i > 0) %> ,<%end %><%=bean.properties[i].column%> 
		 <% end %> 
		 from <%=bean.table%> 
		 <dynamic prepend = "where">< 
		 % bean.properties.each() do |p|%> 
		  	 <isNotEmpty prepend=" and "  property ="<%=p.field%> 
		  	"> <%=p.column%> = #<%=p.field%># < 
		  	 /isNotEmpty> <%end%> 
		 </dynamic> 	
	 </select> 
	
	 <insert id="insert<%=bean.domain%> 
	"	 parameterClass="<%=bean.domain%>Domain"> 
		 insert into 
		 <%=bean.table%>(<% bean.properties.each_index() do |i|%> 
		 <%if(i > 0) %>,<%end %>< 
		 %=bean.properties[i].column%><% end %>) 
		 values(<% bean.properties.each_index() do |i|%>< 
		 %if(i > 0) %>,<%end %>#<%=bean.properties[i].field%> 
		 #<% end %>) 		 
	 </insert> 

	 <delete id="delete<%=bean.domain%> 
	"	 parameterClass="<%if(bean.hasMultiplePK())%><%=bean.domain%> 
	 Domain<%else%>string<%end%>"> 
		 delete from <%=bean.table%>  	
		 where <%=where%> 		
	 </delete> 			

	 <update id="update<%=bean.domain%>"	 parameterClass="< 
	 %=bean.domain%>Domain"> 
		 update <%=bean.table%>   <% isbegin = false %> 
		 set <% bean.properties.each() do |p|%><%if(!p.isPK()) %> 
		 <%if(isbegin)%> ,<%end%><%=p.column%>=#< 
		 %=p.field%>#<%isbegin =true end%><% end %> 	
		 where <%=where%> 	
	 </update> 

 </sqlMap> 

使用 USER 数据模型作为输入,调用上述模板,得到的输出如清单 16 所示。


清单 16. UserDao.xml
				
 <?xml version="1.0" encoding="UTF-8"?> 
 <!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
	"http://ibatis.apache.org/dtd/sql-map-2.dtd"> 
 <sqlMap namespace="com.test.example.dao.UserDao"> 

	 <typeAlias alias="UserDomain"
		 type="com.test.example.domain.UserDomain" /> 

	 <resultMap class="UserDomain" id="UserDomain"> 
			 <result property="id" column="ID" /> 
			 <result property="name" column="NAME" /> 
			 <result property="idcard" column="IDCARD" /> 
			 <result property="address" column="ADDRESS" /> 
				
	 </resultMap> 
    
	 <select id="findUserById" parameterClass="string" resultMap="UserDomain"> 
		 select ID ,NAME ,IDCARD ,ADDRESS 	
		 from USER where ID=#id# 	
	 </select> 

        <select id="findUserByWhere" parameterClass="UserDomain" 
        resultMap="UserDomain"> 
		 select  ID ,NAME ,IDCARD ,ADDRESS 
		 from USER 
		 <dynamic prepend = "where"> 
		  <isNotEmpty prepend=" and "  property ="id"> 
		  ID = #id# </isNotEmpty> 
		  <isNotEmpty prepend=" and "  property ="name"> 
		  NAME = #name# </isNotEmpty> 
		 <isNotEmpty prepend=" and "  property ="idcard"> 
		 IDCARD = #idcard# </isNotEmpty> 
		 <isNotEmpty prepend=" and "  property ="address"> 
		 ADDRESS = #address# </isNotEmpty> 
		 </dynamic> 	
	 </select> 
	
	 <insert id="insertUser"	 parameterClass="UserDomain"> 
		 insert into 
		 USER(ID,NAME,IDCARD,ADDRESS) 
		 values(#id#,#name#,#idcard#,#address#) 		 
	 </insert> 

	 <delete id="deleteUser"	 parameterClass="string"> 
		 delete from USER  	
		 where ID=#id# 		
	 </delete> 			

	 <update id="updateUser"	 parameterClass="UserDomain"> 
		 update USER   
		 set NAME=#name# ,IDCARD=#idcard# ,ADDRESS=#address# 	
		 where ID=#id# 	
	 </update> 

 </sqlMap> 

DAO 实现中,主要的功能在于调用上一步中的产生的 SQL,此外 DAO 类还继承框架中的特定类。详细的 DAO 类模板如清单 17 所示。


清单 17. DAO 类模板
				
 /* 
 * 项目名称:<%=projectName%> 
 * 
 * 创建日期 : <%=date%> 
 * 
 * 1.0.0 版 (<%=bean.author%>)
 */ 
 package com.<%=projectPackage%>.<%=bean.package%>.dao; 

 import java.util.List; 

 import myframe.core.business.daosupport.AbstractPtxDaoSupport; 
 import myframe.core.business.daosupport.IBatisPersistenceManager; 
 import myframe.core.business.daosupport.page.Page; 
 import myframe.core.business.daosupport.page.PageInfo; 

 import com.<%=projectPackage%>.<%=bean.package%> 
 .domain.<%=bean.domain%>Domain; 

 /** 
 * <%=bean.desc%>Dao 
 * @author <%=bean.author%> 
 */ 
 public class <%=bean.domain%>Dao extends 
		 AbstractPtxDaoSupport<IBatisPersistenceManager> { 
	 /** 命名空间 */ 
	 private final static String NAMESPACE = "com.< 
	 %=projectPackage%>.<%=bean.package%>.dao.<%=bean.domain%>Dao."; 
	 /** 
	 * 根据主键查询 <%=bean.desc%> 信息
	 * 
	 * @param id  依据该主键属性查询
	 * @return one <%=bean.domain%>Domain 
	 */ 
	 public <%=bean.domain%>Domain findById(String id) { 
	 return (<%=bean.domain%>Domain) 
	 getPersistanceManager().load(NAMESPACE + "find<%=bean.domain%>ById", id); 
	 } 
	 /** 
	 * 根据指定条件查询 <%=bean.desc%> 信息
	 * 
	 * @param domain  依据 domain 中的非空属性查询
	 * @return a list of <%=bean.domain%>Domain 
	 */ 
	 public List<<%=bean.domain%>Domain> findByWhere(< 
	 %=bean.domain%>Domain domain) { 
		 return getPersistanceManager().find(NAMESPACE + "find< 
		 %=bean.domain%>ByWhere", domain); 
	 } 
	
	
	 /** 
	 * 保存 <%=bean.desc%> 信息
	 * 
	 * @param domain 被保存的 domain 
	 */ 
	 public void insert(<%=bean.domain%>Domain domain) { 
		 getPersistanceManager().insert(NAMESPACE + "insert< 
		 %=bean.domain%>", domain); 
	 } 
	 /** 
	 * 删除 <%=bean.desc%> 信息
	 * 
	 * @param id 依据该主键属性删除
	 */ 
	 public void delete(String id) { 
		 getPersistanceManager().delete(NAMESPACE + "delete< 
		 %=bean.domain%>", id); 
	 } 

	 /** 
	 * 更新 <%=bean.desc%> 信息
	 * 
	 * @param domain 被更新的 domain 	
	 */ 
	 public void update(<%=bean.domain%>Domain domain) { 
		 getPersistanceManager().update(NAMESPACE + "update< 
		 %=bean.domain%>", domain); 

	 } 	
 } 

使用 USER 数据模型作为输入,调用上述模板,得到的输出如清单 18 所示。


清单 18. UserDao.java
				
 /* 
 * 项目名称:XX 系统
 * 
 * 创建日期 : 2011-12-08 
 * 
 * 1.0.0 版 (generator)
 */ 
 package com.test.example.dao; 

 import java.util.List; 

 import myframe.core.business.daosupport.AbstractPtxDaoSupport; 
 import myframe.core.business.daosupport.IBatisPersistenceManager; 
 import myframe.core.business.daosupport.page.Page; 
 import myframe.core.business.daosupport.page.PageInfo; 

 import com.test.example.domain.UserDomain; 

 /** 
 * 用户 Dao 
 * @author generator 
 */ 
 public class UserDao extends 
		 AbstractPtxDaoSupport<IBatisPersistenceManager> { 
	 /** 命名空间 */ 
	 private final static String NAMESPACE = "com.test.example.dao.UserDao."; 

	
	 /** 
	 * 根据主键查询用户信息
	 * 
	 * @param id  依据该主键属性查询
	 * @return one UserDomain 
	 */ 
	 public UserDomain findById(String id) { 
	 return (UserDomain)getPersistanceManager().load(NAMESPACE + "findUserById", id); 
	 } 
	

	 /** 
	 * 根据指定条件查询用户信息
	 * 
	 * @param domain  依据 domain 中的非空属性查询
	 * @return a list of UserDomain 
	 */ 
	 public List<UserDomain> findByWhere(UserDomain domain) 
	 { 
	 return getPersistanceManager().find(NAMESPACE + "findUserByWhere", domain); 
	 } 
	
	 /** 
	 * 保存用户信息
	 * 
	 * @param domain 被保存的 domain 
	 */ 
	 public void insert(UserDomain domain) { 
		 getPersistanceManager().insert(NAMESPACE + "insertUser", domain); 
	 } 
	
	 /** 
	 * 删除用户信息
	 * 
	 * @param id 依据该主键属性删除
	 */ 
	 public void delete(String id) { 
		 getPersistanceManager().delete(NAMESPACE + "deleteUser", id); 
	 } 
	

	 /** 
	 * 更新用户信息
	 * 
	 * @param domain 被更新的 domain 	
	 */ 
	 public void update(UserDomain domain) { 
		 getPersistanceManager().update(NAMESPACE + "updateUser", domain); 
	 } 	
 } 

在清单 8 中,我们给出了服务接口的模板,服务的实现也是很容易完成的,在每一个方法的实现中,先完成相关的业务逻辑并调用 DAO 完成持久化。在我们的生成器中,由于业务逻辑是业务紧密相关的,在生成器的输入中,也没有业务逻辑的描述,所以我们目前只负责生成 DAO 调用等支架代码,剩余的业务逻辑由开发人员再自行补充。

根据之前 DAO 类模板和 Service 接口模板,很容易完成 Service 实现和 Web Action 的模板,这里不再一一给出示例。此外在 JSP 模板中,主要是将 Web 页面中和领域模型相关的元素,用 BeanDef 中的 properties 进行相应的替换,类似于清单 13 领域对象类的模板。


结束语

代码生成器的作用是显而易见的,生成的代码质量较高,代码风格上比较统一,易于修改和维护。此外代码生成器生成代码的效率比手工编写代码的效率高出很多个数量级,使用生成器使得我们可以节省时间专注于业务逻辑的开发上。

在使用代码生成器时,请注意以下原则:

  1. 软件开发的标准化。标准化是能否实现代码生成的基础,一个非标准化的例子是,在一个系统中相同的功能需求使用了不同的方法去实现。为了达到标准化,必须杜绝这种现象的存在,在多个方案中选取最优的一个,达到系统的整体统一。
  2. 不要忘记抽象。不能因为使用了代码生成器提高了代码开发的效率,而不注重对代码的组织和提炼,从而导致生成的代码低层次的重复。代码生成器的使用和组件级框架的使用并不矛盾,结合成熟的组件级框架使用代码生成器,取得的效果往往更好。在我们的 JavaEE 实践中,采用了 Struts + Spring + IBatis 框架,在框架的基础上,结合应用的特点做了相应的抽象和封装,使用了代码生成器,取得了不错的实践效果。

参考资料

学习

讨论

作者简介

高尚是一名软件开发工程师,具有 6 年的软件从业经验,在 Java 开发和财务软件方面积累了一些经验,对编译技术和 Java 技术具有浓厚兴趣,在日常工作中喜欢使用 Ruby、Groovy 等脚本语言解决各类问题。可以通过 [email protected] 与作者联系。

李莉是一名软件开发工程师,具有 3 年的软件从业经验,在 Java 开发和 WAS 应用服务器方面积累了一些经验,对 Web 应用服务器技术具有浓厚兴趣。可以通过 [email protected] 与作者联系。

你可能感兴趣的:(Ruby)