2.6 编写代码生成器
计算机的专家们一直在探寻一种能使得重复代码越来越少的方法,函数封装、面向对象、AOP、MDA、ORM……所有这些相关或者无关的技术都在试图将重复的代码消灭,可是一路走过来,人们突然发现,重复的代码是不可能被完全消灭的,到了更高的层次一定会有更高级的重复的代码需要我们去对付,因此代码生成也逐渐不再被妖魔化。网页编辑器、编译器、IDE等这些非常重要的工具不就是代码生成器吗?只要是系统经过好的设计,对于剩下的一些重复性的代码与其使用学院派且严重影响性能的方法进行消除,不如使用代码生成器来完成来得更实在一些。
回到现实中来,在我们开发程序的过程中,特别是开发一些业务系统的过程中,一些重复的代码总是不可避免的,比如ORM中POJO代码和配置文件、资料录入界面的代码、数据库DDL语句等,这些工作如果要开发人员去手动完成话,不仅会降低开发效率,而且会带来很多bug,最重要的是极容易使得开发人员产生厌倦心理从而消极怠工甚至离职,从而提高了项目的人力资源成本、增大了项目的风险。因此在大一些的开发团队中都在使用着各种或公开或自酿的代码生成工具,而且越来越多的人开始选择自酿工具,这是因为使用第三方的代码生成工具往往不能满足自己的个性化需求。
我们可以通过多种方式来写代码生成工具,比如最简单的通过StringBuffer拼字符串,或者借助groovy template、velocity等工具来完成,这些工具各有千秋,不过由于本书是讲解Eclipse的,因此我们就来看一下在Eclipse中有哪些代码生成方案。
1. 使用StringBuffer拼接来生成代码
在一些比较简单的代码生成中,这样的方式是比较方便的,但是当生成的代码结构变得越来越复杂的时候,代码中stringbuffer.append()与逻辑判断代码搅和在一起,程序变得非常难以维护。
2. 使用JDT API中的AST
JDT会把Java代码编译成AST(Abstract Syntax Tree 抽象语法树),这样复杂的Java代码就变成了相对简单的树状结构,我们就可以通过AST来遍历Java代码,从而解析代码或者对代码进行修改,Eclipse中的Java代码重构就是基于AST来进行的。
在Eclipse中AST被称为CompilationUnit,对应的接口就是ICompilationUnit,通过Java代码来生成CompilationUnit最简单的方法是使用 IPackageFragment.createCompilationUnit。指定编译单元的名称和内容,于是在包中创建了编译单元,并返回新的 ICompilationUnit。我们还可以从头创建一个CompilationUnit,即生成一个不依赖于Java代码的CompilationUnit,然后在这个CompilationUnit上添加类、添加方法、添加代码,然后调用JDT的AST解析器将CompilationUnit输出成Java代码。这种方式是最严谨的方式,但是当要生成的代码比较复杂的时候程序就变得臃肿无比,而且只能生成Java代码,不能生成XML配置文件等文件。
3. 使用JET
JET是Eclipse中一个非常强大的代码生成工具,使用JET你可以运用类似JSP一样的语法,这样我们就可以轻松地编写代码模板。用它可以创建SQL语句、XML、Java源代码等文件的代码生成器。本书将把它作为代码生成的工具,因此我们在此处重点讲解JET的使用。JET是EMF的一部分,要使用它必须首先安装EMF插件。
使用JET分为如下几步。
(1) 把项目转化成JET项目
要在项目中使用JET,必须首先把它转化成JET项目,方法如下。
① 在【包资源管理器】视图上右击,在弹出的快捷菜单中选择【新建】|【其他】命令,然后在弹出的对话框中选择Java Emitter Templates下的Convert Projects to JET Projects选项,如图2.21所示。
② 单击【下一步】按钮,选择要转化的项目,如图2.22所示,然后单击【完成】按钮。向导会在项目的根目录下创建一个名字为templates的文件夹,而且给项目添加了一个JET Builder,这个构建器会自动将templates文件夹下的模板文件进行编译,生成代码。
(2) 设置JET
在项目上右击,在弹出的快捷菜单中选择【属性】命令,打开JET Settings选项卡,在这个选项卡中就可以修改模板文件夹和源文件夹了,如图2.23所示。注意此处必须输入源文件夹的名字,否则在生成代码的时候就有可能出现代码生成位置出错的问题。

图2.21 选择JET转换向导 图2.22 选择被转换的项目

图2.23 设置JET的属性
(3) 创建模板文件
JET的模板文件的命名规定是在要生成的代码生成器类的文件名后加jet,比如想命名我们的代码生成器为MyGen.java,那么只要把模板命名为MyGen.javajet就可以了。因此可在templates文件夹下创建一个文件EnumCodeGenerator.javajet,创建完毕之后,系统会弹出一个错误对话框,如图2.24所示。

图2.24 构建出错对话框
不要惊慌,这并不是说明我们的创建过程有错,而是创建完模板文件以后,JET构建器就去尝试构建EnumCodeGenerator.javajet,由于这个文件是空的,所以当然就构建失败报错了。
在EnumCodeGenerator.javajet中输入如下代码:
<%@ jet package="com.cownew.enumgenerator.wizards" class="EnumCodeGenerator" %> Hello,<%=argument%>!
保存以后,JET就立即会生成EnumCodeGenerator.java文件,内容如下:
public class EnumCodeGenerator
{
protected static String nl;
public static synchronized EnumCodeGenerator create(
String lineSeparator)
{
nl = lineSeparator;
EnumCodeGenerator result = new EnumCodeGenerator();
nl = null;
return result;
}
protected final String NL = nl == null ?
(System.getProperties().getProperty("line.separator")) : nl;
protected final String TEXT_1 = " Hello, ";
protected final String TEXT_2 = "!";
protected final String TEXT_3 = NL;
public String generate(Object argument)
{
final StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(TEXT_1);
stringBuffer.append(argument);
stringBuffer.append(TEXT_2);
stringBuffer.append(TEXT_3);
return stringBuffer.toString();
}
}
可以看到JET生成的代码采用的也是StringBuffer拼装的形式,注意此处生成的代码是无法手工修改的,因为每次修改以后保存的时候JET会自动把代码替换成未修改之前的代码。
(4) 测试模板代码
在EnumCodeGenUtils中创建main方法,然后输入如下代码:
EnumCodeGenerator gen = new EnumCodeGenerator();
System.out.println(gen.generate("Eclipse"));
运行之后控制台中就打印出了:Hello, Eclipse!
我们来对上面的模板代码和测试代码做一下简要的分析:
① <%@ jet package="com.cownew.enumgenerator.wizards" class="EnumCodeGenerator" %>
这是模板的头部分,以“@ jet”开头,这部分主要声明此模板的有关信息,比如生成代码的包路径、类名、导入的类等,package属性定义的就是生成代码的包路径,而class属性定义的是生成的类名。
② Hello, <%=argument%>!
这部分就是模板的正文了,和JSP语法一样,显示一个变量的方法是<%=变量名>。注意这里的变量argument是有特殊含义的,它表示传递给模板的参数。
③ Object参数
代码生成器生成代码的方法是generate,因为我们经常需要传递一些参数给代码生成器,所以generate方法有一个类型为Object的参数,此参数在模板中可以用argument取得。
对JET有了一个感性的认识之后,我们就来通过实战来操练一下。上一节中EnumCodeGenUtils.getEnumSourceCode方法的实现为空,这一节我们就来完成这项关键性的工作。
经过分析,我们发现需要传递给模板代码如下3个参数才可以正确地输出代码:枚举类的包名、枚举类的类名、枚举类的项。因为模板代码的generate方法只接受类型为Object的一个参数,所以我们需要把这3个参数封装到一个JavaBean中,如下定义JavaBean。
【代码2-14】模板参数类:
public class EnumGenArgInfo
{
private Set<String> items;
private String className;
private String packageName;
public String getPackageName()
{
return packageName;
}
public void setPackageName(String packageName)
{
this.packageName = packageName;
}
public String getClassName()
{
return className;
}
public void setClassName(String className)
{
this.className = className;
}
public Set<String> getItems()
{
return items;
}
public void setItems(Set<String> items)
{
this.items = items;
}
}
接下来我们来写模板文件。
【代码2-15】模板文件:
<%@ jet package="com.cownew.enumgenerator.wizards"
class="EnumCodeGenerator"
imports="java.util.*"
%>
<%
EnumGenArgInfo argInfo = (EnumGenArgInfo)argument;
Set<String> enumItems = argInfo.getItems();
String className = argInfo.getClassName();
String packageName = argInfo.getPackageName();
%>
package <%=packageName%>;
public class <%=className%>
{
private String type;
<%for(String item:enumItems){%>
public <%=className%> <%=item%> = new <%=className%>("<%=item%>");
<%}%>
private <%=className%>(String type)
{
super();
this.type = type;
}
public int hashCode()
{
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((type == null) ? 0 : type.hashCode());
return result;
}
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final <%=className%> other = (<%=className%>) obj;
if (type == null)
{
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
}
这个模板文件是非常简单的,有了前面的基础,读懂这个模板文件就非常简单了,这里只讲两点:
l 文件头的imports属性是用来定义生成的代码的import列表的,这个模板中用到了集合类Set,所以要用imports="java.util.*" 将其导入,否则生成的代码会编译错误。如果要导入多个类,只要把它们用空格隔开即可,比如:
imports= imports="java.util.* java.sql.Date"
不能使用其他分隔符。
l 由于传递进来的参数是一个JavaBean,因此需要把argument进行一次转型操作:
EnumGenArgInfo argInfo = (EnumGenArgInfo)argument;
编写下面的代码测试一下这个代码模板:
public static void main(String[] args)
{
EnumCodeGenerator gen = new EnumCodeGenerator();
EnumGenArgInfo argInfo = new EnumGenArgInfo();
argInfo.setClassName("MyEnum");
Set<String> items = new HashSet<String>();
items.add("VIP");
items.add("MM");
argInfo.setItems(items);
argInfo.setPackageName("com.cownew");
System.out.println(gen.generate(argInfo));
}
运行之后发现输出的代码完全正确。
这样我们就可以来完成EnumCodeGenUtils类的getEnumSourceCode方法。
【代码2-16】完成后的getEnumSourceCode方法:
public static String getEnumSourceCode(String packageName, String fileName,
Set<String> itemDefSet)
{
Pattern pattern = Pattern.compile("(.+).java");
Matcher mat = pattern.matcher(fileName);
mat.find();
String className = mat.group(1);
EnumCodeGenerator gen = new EnumCodeGenerator();
EnumGenArgInfo argInfo = new EnumGenArgInfo();
argInfo.setClassName(className);
argInfo.setItems(itemDefSet);
argInfo.setPackageName(packageName);
return gen.generate(argInfo);
}
这里用到了正则表达式来从Java文件名中提取类名,使用的是JDK中的正则表达式实现,对于正则表达式,我们可以去查阅相关资料,正则表达式是一个非常好用的工具,掌握以后能轻松解决很多字符串解析相关的问题,并为学习编译原理打下基础。







