代码生成器的设计理念和实践
李俊杰
概论
代码生成器十分有利于提高编码效率和项目进度,并且有利于代码的规范化管理,把程序员从繁琐的重复性的代码编写中解放出来,特别是同时产生代码和配置文件(config),这样避免了多个文件之间的互相引用带来的错误(当你查找bug精疲力竭,最后发现仅仅是由于你的配置文件中的配置时某一个参数的字母大小写的错误,你什么感受),所以颇受的广大程序员和项目管理者的青睐。
设计思想分类
代码生成器的设计思路其实很简单。简而言之,包括输入模块,加工模块,输出模块。
输入模块
所谓输入模块主要是组织“原料”,该原料可以是数据库表(如hibernate的代码生成器),也可以是文本文件(最常用的是xml文件,或者java代码文件如doclet代码生成器)或者别的途径。
加工模块
加工模块是根据输入模块产生的原料按照一定的规范(模版)生成字符串(广义的字符串,即包括要生成文件的内容 )。
输出模块
输出模块的功能是把代码生成器生成的代码字符串写入一系列指定目录和文件名称文件。
设计分类
1) 无模版设计类型。事实上该模版在设计者的心中,也就是说设计者非常清楚生成的文件的格式和内容。后面的篇幅中会简单介绍该设计思想,其优点是很灵活,缺点是扩展性和适应性较差。如同样的“原料“,生成新格式的文件就会很被动,需要重新编程。
2) 模版设计类型。这是现在最广泛的的运用设计思路。就是加工模块首先读模版文件,根据模版文件中的标志,把相应的“原料“填充到替换标志。然后生成新的文档。
实现原理简介
1)无模版设计类型示例:
//设置文件中的格式
//回车
public static final String RETURN_ROW = "\n";
//2个空格
public static final String BLANK2 = " ";
//4个空格
public static final String BLANK4 = BLANK2 + " ";
//6个空格
public static final String BLANK6 = BLANK4 + " ";
public static final String BLANK8 = BLANK6 + " ";
public static final String BLANK10 = BLANK8 + " ";
public static final String BLANK12 = BLANK10 + " ";
public static final String BLANK14 = BLANK12 + " ";
public static final String BLANK16 = BLANK14 + " ";
public static final String BLANK18 = BLANK16 + " ";
public static final String BLANK20 = BLANK18 + " ";
public static final String BLANK22 = BLANK20 + " ";
public static final String BLANK24 = BLANK22 + " ";
下面是读。Xml文件,根据读进来的原料,进行加工。
String serviceID = serviceConfig.getServiceID();
String serviceName = serviceConfig.getServiceName();
String serviceDesc = serviceConfig.getServiceDesc();
String serviceTypeName = serviceConfig.getServiceTypeName();
String packageName = serviceConfig.getPackageName();
String pureServiceName = serviceName.substring(0,serviceName.lastIndexOf("Service"));
String serviceClassName = packageName + "." + serviceName;
String inServiceBeanClassName = packageName + "." + pureServiceName + "InServiceBean";
String outServiceBeanClassName = packageName + "." + pureServiceName + "OutServiceBean";
String serviceUnitTestClassName = packageName + "." + pureServiceName + "ServiceUnitTest";
String canTest = serviceConfig.getCanTest();
String noSecurityCheck = serviceConfig.getNoSecurityCheck();
String dbServerName = serviceConfig.getDbServerName();
String serviceNo = serviceConfig.getServiceNo();
//下面是生成新的字符串
StringBuffer sb = new StringBuffer();
sb.append(beginTagRow(ProxyXMLCodeGenerator.BLANK2,"service"));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceNo",serviceNo));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceID",serviceID));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceName",serviceName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceDesc",serviceDesc));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceTypeName",serviceTypeName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"packageName",packageName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceClassName",serviceClassName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"inServiceBeanClassName",inServiceBeanClassName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"outServiceBeanClassName",outServiceBeanClassName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"serviceUnitTestClassName",serviceUnitTestClassName));
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"canTest",canTest));
sb.append(ProxyXMLCodeGenerator.BLANK4).append("<noSecurityCheck>1</noSecurityCheck>").append(ProxyXMLCodeGenerator.RETURN_ROW);
sb.append(generateRow(ProxyXMLCodeGenerator.BLANK4,"dbServerName",dbServerName));
sb.append(ProxyXMLCodeGenerator.BLANK4).append("<callServices/>").append(ProxyXMLCodeGenerator.RETURN_ROW);
System.out.println(sb.toString());
这样就生成了某一定格式的xml的字符串。
2)模版设计类型示例
小试牛刀,最基础的替换原理
public static String transit(HashMap map, String template){
if(template == null){
return null;
}
String ret;
StringBuffer sb = new StringBuffer();
int beginIndex = 0;
int endIndex = -1;
int t = 0 ;
int i = 0;
String key = null;
while((t = template.indexOf("$",beginIndex)) != -1 ){
endIndex = t;
if(beginIndex + 1 == endIndex){
beginIndex = endIndex + 1;
continue;
}
key = template.substring(beginIndex, endIndex);
i++;
if(i % 2 == 1){
sb.append(key);
}else{
sb.append((String)map.get(key));
}
beginIndex = endIndex + 1;
}
if((t = template.lastIndexOf("$"))!= -1){
sb.append(template.substring(t+1));
}else{
sb.append(template);
}
ret = sb.toString();
return ret;
}
public static void main(String[] args){
HashMap map = new HashMap();
map.put("name","lisi");
map.put("age","25");
map.put("address","beijing");
map.put("year"," years old");
map.put("desrc","Read me : ");
String[] names = new String[]{"name", "age","address","year","desrc"};
String[] values = new String[]{"lisi", "25", "beijing"," years old","Read me : "};
String template = "$desrc$My name is $name$, my old is $age$$year$, I live in $address$.";
System.out.println(StringUtils.transit(map, template));
}
下面是工业化版本
//seq语句模版
#loopBegin#
INSERT INTO "TX"(
"TXID",
"DEPT_CLASS",
"TXNAME",
"SEP_GRANT_FLAG",
"CROSS_OP_FLAG",
"MGRBRANCH_OP_FLAG",
"SECURITY_CHK_FLAG",
"SECURITY_TX_FLAG",
"VALCONSTR_FLAG",
"TX_TYPE",
"CI_GROUP_NO"
)
VALUES(
'$serviceId$',
'$deptClass$',
'$serviceDesc$',
'1',
'1',
'1',
'1',
'1',
'0',
'U',
'1'
)
/
#loopEnd#
首先我要解释一下该模版中的标志#loopBegin#和#loopEnd#表示这中间的代码要进行循环,其中“$$,##”为识别符,因为在模版中要识别该代码段要进行特殊的操作或者替换(另外识别符的选择一般是比较生僻的字符,在原始的代码中难以看到,如果原始的代码中真的存在该字符,可以用转字符来替代,如“\$,\#”),$serviceId$表示把“原料”中的serviceId的值替换到该处,生成新的代码字符串。
实现代码(下面的代码是我从相关的类中摘取的代码),我们的模版是以文件存在的,而文件是由一行行字符串组成的,因此我把处理的最小单元设计为一行。
/**
*
* @author lijunjie
* @param
* @return String
* @exception
* 方法描述:把从模板文件中读取的某一行进行转换,把其中的参数替换成实际的代码
*/
public String transit(ArrayList lines,HashMap map){
StringBuffer sb = new StringBuffer();
String line = (String)lines.get(0);
StringTokenizer tokenizer = new StringTokenizer(line,"$");
int i = 0;
String name;
while(tokenizer.hasMoreTokens()){
i++;
if(i % 2 == 1){
sb.append(tokenizer.nextToken());
}else{
if((name = (String)map.get(tokenizer.nextToken())) != null){
sb.append(name);
}
}
}
return sb.toString();
}
/**
*
* @author lijunjie
* @param
* @return String
* @exception
* 方法描述:依据模板生成文件内容的字符串,
* 根据模板文件的内容来进行不同的处理,使用策略模式来创建不同的处理类.
*/
public String generateCode(String templateFile) {
StringBuffer sb = new StringBuffer();
ArrayList lines= new ArrayList();
try{
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(templateFile)));
String line = null;
int state = 0;
while((line = reader.readLine()) != null){
//单行非循环的代码的处理
if((line.indexOf("#loopBegin#") == -1) && (line.indexOf("#loopEnd#") == -1)){
LinesOperator operator = new SingleLineOperator();
lines.add(line);
sb.append(operator.transit(lines,map)).append("\n");
lines.clear();
}
//处理循环代码块,即类的属性列表和get,set方法,首先把需要循环处理的代码块从模板中读出
if(line.indexOf("#loopBegin#") != -1){
while((line = reader.readLine()) != null){
if(line.indexOf("#loopEnd#") == -1){
lines.add(line);
}else{
LinesOperator operator= new LoopBlockOperator();
sb.append(operator.transit(lines,map));
lines.clear();
break;
}
}
}
}
reader.close();
}catch(Exception ex){
ex.printStackTrace();
}
return sb.toString();
}
架构设计
.bmp)
以上是代码生成期类图,其中单箭头表示依赖,双箭头表示继承。
架构简介
如图所示,CodeGeneratorClient类是客户端,CodeGeneratorFactory是代码生成器工厂类,功能是根据客户端的不同条件,产生创建CodeGenerator(代码生成器)子类,CodeGenerator类调用LineOperator类的子类处理模版文件中的循环(LoopBlockOperator)或者单行替换(SingleLineOperator)操作。
在CodeGeneratorFactory工厂类中,由于该实例在系统中只需要一个就行,所以使用了单例模式getCodeGeneratorFactory(),根据输入的要产生的不同的文件类型,创建不同的CodeGenerator对象(根据java多态,实际上是创建不同的CodeGenerator子类对象), 然后再根据读取的xml文件和相关条件,产生并返回CodeGenerator列表,getInstances(String inputXMlFile, HashMap condition),其中根据condition中的“classType”属性,来创建不同的代码生成器子类。
在CodeGenerator类中,public abstract ArrayList getCodeGenerators(String inputXMlFile, HashMap condition)抽象方法是实现组织“原材料”的功能,其所有的子类必须实现的,因为不同的“原材料“来源,其组织材料的逻辑也千差万别,所以只有具体子类实现更合适。
在CodeGenerator类中,String generateCode(String templateFile)方法的功能是,依据模板生成文件内容的字符串, 根据模板文件的内容来进行不同的处理,使用策略模式来创建不同的处理类。根据需要循环操作来创建LoopBlockOperator类,来进行循环把原材料填充到字符串中。根据单行操作来创建SingleLineOperator类把原材料填充到某一行字符串。
在CodeGenerator类中,public void createFile(String createDir, String content)的功能是创建文件,根据文件存放目录和java包名称,创建实际生成文件的目录,然后把文件内容写入要创建的文件中,其中createDir表示要生成的文件的全名(即带有目录的文件名称),如果目录不存在,则创建目录,然后创建文件并写入文件内容(condition)。
最后,简要介绍我写的几个CodeGenerator具体子类的功能,TxInsertSqlGenerator产生insert语句的。TableBeanGenerator根据数据库表结构产生java bean,即根据数据库表中的字段名称,汉语注释和英文名称,生成get,set方法及注释,当然把数据库的英文名称的下划线等改变成符合java格式的属性名称,和get,set格式的方法名称。 TableXMLGeneratorByPDM子类的功能是从power designer设计数据库表的.pdm文件中读取原材料,因为.pdm的实际组织格式也时xml格式,所以读文件非常方便,产生全新格式的xml文件,当然其中的表的字段英文名称也进行相应的符合java规则的改变。
总结,该设计架构是一个扩展性极好的架构,如果有新的需求,只须扩展CodeGenerator类的子类,在CodeGeneratorFactory类注册后,就能很方便地应用。
努力,在于我热爱我的事业,与中国的软件一起走向成熟,走向世界。
联系作者:lijj_72@hotmail.com
发表于 @ 2005年10月21日 23:35:00|评论(loading...)|编辑