使用 Eclipse Modeling Framework 进行建模,第 2 部分
Eclipse 的 Java Emitter Templates(JET) 是一个开放源代码工具,可以在 Eclipse Modeling Framework(EMF)中生成代码。 JET 与 JSP 非常类似,不同之处在于 JET 功能更强大,也更灵活,可以生成 Java、 SQL 和任何其他语言的代码,包括 JSP。本文将介绍如何创建和配置 JET,并将其部署到各种环境中。
Java Emitter Templates(JET) 概述
开发人员通常都使用一些工具来生成常用的代码。 Eclipse 用户可能对一些标准的工具非常熟悉,这些工具可以为选定的属性生成 for(;;) 循环, main() 方法,以及选定属性的访问方法。将这些简单而机械的任务变得自动化,可以加快编程的速度,并简化编程的过程。在某些情况中,例如为 J2EE 服务器生成部署代码,自动生成代码就可以节省大量时间,并可以隐藏具体实现特有的一些复杂性,这样就可以将程序部署到不同的 J2EE 服务器上。自动生成代码的功能并不只是为开发大型工具的供应商提供的,在很多项目中都可以使用这种功能来提高效率。 Eclipse 的 JET 被包装为 EMF 的一部分,可以简单而有效地向项目中添加自动生成的代码。本文将介绍在各种环境中如何使用 JET 。
JET 与 JSP 非常类似:二者使用相同的语法,实际上在后台都被编译成 Java 程序;二者都用来将呈现页面与模型和控制器分离开来;二者都可以接受输入的对象作为参数,都可以在代码中插入字符串值(表达式),可以直接使用 Java 代码执行循环、声明变量或执行逻辑流程控制(脚本);二者都可以很好地表示所生成对象的结构,(Web 页面、Java 类或文件),而且可以支持用户的详细定制。
JET 与 JSP 在几个关键的地方存在区别。在 JET 中,可以变换标记的结构来支持在不同的语言中生成代码。通常 JET 程序的输入都是一个配置文件,而不是用户的输入(当然也不禁止这样使用)。而且对于一个给定的工作流来说,JET 通常只会执行一次。这并不是技术上的限制;您可以看到 JET 有很多完全不同的用法。
开始
要使用 JET,创建一个新 Java 项目 JETExample ,并将源文件夹设置为 src 。为了让 JET 启用这个项目,请点击鼠标右键,然后选择 Add JET Nature。这样就会在新项目的根目录下创建一个 templates 目录。JET 的缺省配置使用项目的根目录来保存编译出来的 Java 文件。要修改这种设置,打开该项目的 properties 窗口,选择 JET Settings,并将 source container 设置为 src 。这样在运行 JET 编译器时,就会将编译出来的 JET Java 文件保存到这个正确的源文件夹中。
现在我们已经准备好创建第一个 JET 了。JET 编译器会为每个 JET 都创建一个 Java 源文件,因此习惯上是将模板命名为 NewClass.javajet ,其中 NewClass 是要生成的类名。虽然这种命名方式不是强制的,但是这样可以避免产生混乱。
首先在模板目录中创建一个新文件 GenDAO.javajet 。这样系统会出现一个对话框,警告您在这个新文件的第 1 行第 1 列处有编译错误。如果您详细地看以下警告信息,就会发现它说 "The jet directive is missing"(没有 jet 指令)。虽然这在技术上没有什么错误,因为我们刚才只不过是创建了一个空文件,但是这个警告信息却很容易产生混乱并误导我们的思路。单击 'OK' 关闭警告对话框,然后单击 'Cancel' 清除 New File 对话框(这个文件已经创建了)。为了防止再次出现这种问题,我们的首要问题是创建 jet 指令。
每个 JET 都必须以 jet 指令开始。这样可以告诉 JET 编译器编译出来的 Java 模板是什么样子(并不是模板生成了什么内容,而是编译生成的模板类是什么样子;请原谅,这个术语有些容易让人迷惑)。此处还要给出一些标准的 Java 类信息。例如,在下面这个例子中使用了以下信息:
<%@ jet package="com.ibm.pdc.example.jet.gen" class="GenDAO" imports="java.util.* com.ibm.pdc.example.jet.model.*" %>
清单 1 的内容是真正自解释的。在编译 JET 模板时,会创建一个 Java 文件 GenDAO ,并将其保存到 com.ibm.pdc.example.jet.gen 中,它将导入指定的包。重复一遍,这只是说明模板像什么样子,而不是模板将要生成的内容 -- 后者稍后将会介绍。注意 JET 输出结果的 Java 文件名是在 jet 的声明中定义的,它并不局限于这个文件名。如果两个模板声明了相同的类名,那么它们就会相互影响到对方的变化,而不会产生任何警告信息。 如果您只是拷贝并粘贴模板文件,而没有正确地修改所有的 jet 声明,那就可能出现这种情况。因为在模板目录中创建新文件时会产生警告,而拷贝和粘贴是非常常见的,因此要自己小心这个问题。
JSP 可以通过预先声明的变量(例如会话、错误、上下文和请求)获取信息, JET 与此类似,也可以使用预先声明的变量向模板传递信息。JET 只使用两个隐式的变量: stringBuffer ,其类型为 StringBuffer (奇怪吧?),它用来在调用 generate() 时构建输出字符串;以及一个参数,出于方便起见,我们称之为 argument ,它是 Object 类型。典型的 JET 模板的第一行会将其转换为一个更适合的类,如清单 2 所示。
<% GenDBModel genDBModel = (GenDBModel)argument; %>
package <%= genDBModel.getPackageName() %>;
正如您可以看到的一样,JET 的缺省语法与 JSP 相同:使用 <%...%> 包括代码,使用 <%= ... %> 打印表达式的值。与 JSP 类似,正确地使用 <% ... %> 标签就可以添加任何逻辑循环或结构,就像是在任何 Java 方法中一样。例如:
Welcome <%= user.getName() %>! <% if ( user.getDaysSinceLastVisit() > 5 ) { %> Whew, thanks for coming back. We thought we'd lost you! <% } else { %> Back so soon? Don't you have anything better to do? <% } %>
在定义完 JET 之后,保存文件并在包浏览器中在这个文件上点击鼠标右键,选择 Compile Template。如果一切正常,就会在 com.ibm.pdc.example.jet.gen 包中创建一个类 GenDAO 。其中只有一个方法 public String generate(Object argument) (参见清单 4),这样做的结果就是在 javajet 模板中定义的内容。
清单 4. 一个基本的 JET 编译后的 Java 类,其功能是打印 "Hello <%=argument%>"
1 package com.ibm.pdc.example.jet.gen; 2 import java.util.*; 3 public class GenDAO{ 4 protected final String NL = System.getProperties().getProperty("line.separator"); 5 protected final String TEXT_1 = NL + "Hello, "; 6 protected final String TEXT_2 = NL + " "; 7 public String generate(Object argument){ 8 StringBuffer stringBuffer = new StringBuffer(); 9 stringBuffer.append(TEXT_1); 10 stringBuffer.append( argument ); 11 stringBuffer.append(TEXT_2); 12 return stringBuffer.toString(); 13 } 14 }
编写好模板之后,您可能就会注意到一些公共的元素,这些元数会反复出现,例如所有生成的代码中都添加的版权信息。在 JSP 中,这是通过 include 声明处理的。将所有想要添加的内容都放到一个文件中,并将该文件命名为 'copyright.inc',然后在 javajet 模板中添加 <%@ include file="copyright.inc" %> 语句。所指定的包含文件会被添加到编译后的输出结果中,因此它可以引用到现在为止已经声明的任何变量。扩展名 .inc 可以任意,只是不要采用以 jet 或 JET 结尾的名字,否则将试图编译包含文件,这样该文件的理解性自然很差。
如果只使用包含文件还不能满足要求,您可能会想添加其他一些方法,或者对代码生成过程进行定制;最简单的方法是创建一个新的 JET 骨架。骨架文件就是描述编译后的 JET 模板样子的一个模板。缺省的骨架如清单 5 所示。
1 public class CLASS{ 2 public String generate(Object argument) { 3 return ""; 4 } 5 }
所有的 import 语句都位于最开始, CLASS 会被替换为在 jet 声明的 class 属性中设置的类名, generate() 方法的代码会被替换为执行生成操作的代码。因此,要修改编译后的模板代码的样子,我们只需要创建一个新的骨架文件并进行自己想要的定制即可,但是仍然要在原来的地方保留基本的元素。
要创建一个定制的骨架,在 custom.skeleton 模板目录中创建一个新文件,如清单 6 所示。
1 public class CLASS{ 2 private java.util.Date getDate() { 3 return new java.util.Date(); 4 } 5 6 public String generate(Object argument) { 7 return ""; 8 } 9 }
然后在想要使用这个定制骨架的任何 JET 模板中,向 javajet 文件中的 jet 声明添加 skeleton="custom.skeleton" 属性。
或者,也可以使用它对基类进行扩充,例如 public class CLASS extends MyGenerator ,并在基类中添加所有必要的帮助器方法。这样可能会更加整洁,因为它保留了代码的通用性,并可以简化开发过程,因为 JET 编译器并不能总是给出最正确的错误消息。
定制骨架也可以用来修改方法名和 generate() 方法的参数列表,这样非常挑剔的开发人员就可以任意定制模板。说 JET 要将 generate() 的代码替换为要生成的代码,其实有些不太准确。实际上,它只会替换在骨架中声明的最后一个方法的代码,因此如果粗心地修改骨架的代码,就很容易出错,而且会让您的同事迷惑不解。
正如您可以看到的一样,模板一旦编译好之后,就是一个标准的 Java 类。要在程序中使用这个类,只需要分发编译后的模板类,而不需要分发 javajet 模板。或者,您可能希望让用户可以修改模板,并在启动时自动重新编译模板。EMF 可以实现这种功能,任何需要这种功能或对此感兴趣的人都可以进入 plugins/org.eclipse.emf.codegen.ecore/templates 中,并修改 EMF 生成模型或编辑器的方式。
如果您只是希望可以只分发编译后的模板类,那么编译过程可以实现自动化。迄今为止,我们只看到了如何使用 JET Eclipse 插件来编译 JET 模板,但实际上我们可以编写一些脚本来实现这种功能,或者将生成代码的工作作为一项 ANT 任务。
要让最终用户可以定制模板(以及对模板的调试),可以选择在运行时对模板进行编译。实现这种功能有几种方法,首先我们使用一个非常有用的类 org.eclipse.emf.codegen.jet.JETEmitter ,它可以对细节进行抽象。常见的(但通常是错误的)代码非常简单,如清单 7 所示。
清单 7. JETEmitter 的简单用法(通常是错误的)
String uri = "platform:/templates/MyClass.javajet"; JETEmitter jetEmitter = new JETEmitter( uri ); String generated = jetEmitter.generate( new NullProgressMonitor(), new Object[]{argument} );
如果您试图在一个标准的 main() 方法中运行这段代码,就会发现第一个问题。 generate() 方法会触发一个 NullPointerException 异常,因为 JETEmitter 假设自己正被一个插件调用。在初始化过程中,它将调用 CodeGenPlugin.getPlugin().getString() ,这个函数会失败,因为 CodeGenPlugin.getPlugin() 为空。
解决这个问题有一个简单的方法:将这段代码放到一个插件中,这样的确可以管用,但却不是完整的解决方法。现在 JETEmitter 的实现创建了一个隐藏项目 .JETEmitters ,其中包含了所生成的代码。然而, JETEmitter 并不会将这个插件的 classpath 添加到这个新项目中,因此,如果所生成的代码引用了任何标准 Java 库之外的对象,都将不能成功编译。2.0.0 版本初期似乎解决了这个问题,但是到 4 月初为止,这还没有完全实现。要解决这个问题,必须对 JETEmitter 类进行扩充,使其覆盖 initialize() 方法,并将其加入您自己的 classpath 项中。Remko Popma 已经编写了很好的一个例子 jp.azzurri.jet.article2.codegen.MyJETEmitter (参阅 参考资料),这个例子可以处理这个问题,在 JET 增加这种正确的特性之前都可以使用这种方法。修改后的代码如清单 8 所示。
String base = Platform.getPlugin(PLUGIN_ID).getDescriptor().getInstallURL().toString(); String uri = base + "templates/GenTestCase.javajet"; MyJETEmitter jetEmitter = new MyJETEmitter( uri ); jetEmitter.addClasspathVariable( "JET_EXAMPLE", PLUGIN_ID); String generated = jetEmitter.generate( new NullProgressMonitor(), new Object[]{genClass} );
在命令行中编译 JET 非常简单,不会受到 classpath 问题的影响,这个问题会使编译一个 main() 方法都非常困难。在上面这种情况中,难点并不是将 javajet 编译成 Java 代码,而是将这个 Java 代码编译成 .class 。在命令行中,我们可以更好地控制 classpath,这样可以分解每个步骤,最终再组合起来,就可以使整个工作顺利而简单。唯一一个技巧是我们需要以一种 "无头" 模式(没有用户界面)来运行 Eclipse,但即便是这个问题也已经考虑到了。要编译 JET,请查看一下 plugins/org.eclipse.emf.codegen_1.1.0/test 。这个目录中包含了 Windows 和 Unix 使用的脚本,以及一个要验证的 JET 例子。
有一个 ANT 任务 jetc ,它要么可以采用一个 template 属性,要么对多个模板有一个 fileset 属性。一旦配置好 jetc 任务的 classpath 之后,模板的编译就与标准的 Java 类一样简单。有关如何获取并使用这个任务的更多信息,请参阅 参考资料。
最终,JET 使用 "<%" 和 "%>" 来标记模板,然而这与 JSP 使用的标记相同。如果您希望生成 JSP 程序,那就只能修改定界符。这可以在模板开头的 jet 声明中使用 startTag 和 endTag 属性实现,如清单 9 所示。在这种情况中,我使用 "[%" 和 "%]" 作为开始定界符和结束定界符。正如您可以看到的一样, "[%= expression %]" 可以正确处理,就像前面的 "<%= expression %>" 一样。
<%@ jet package="com.ibm.pdc.example.jet.gen" class="JspGen" imports="java.util.* " startTag = "[%" endTag = "%]" %> [% String argValue = (String)argument; %] package [%= argValue %];
有一个不幸的事实:很多代码都是通过拷贝/粘贴而实现重用的,不管是大型软件还是小型软件都是如此。很多时候这个问题并没有明显的解决方案,即使面向对象语 言也不能解决问题。在重复出现相同的基本代码模式而只对实现稍微进行了一些修改的情况中,将通用的代码放到一个模板中,然后使用 JET 来生成各种变化,这是一种很好的节省时间和精力的办法。JSP 早已采用了这种方法,因此 JET 可以从 JSP 的成功中借鉴很多东西。JET 使用与 JSP 相同的基本布局和语义,但是允许更灵活的定制。为了实现更好的控制,模板可以进行预编译;为了实现更高的灵活性,也可以在运行时编译和分发。
在本系列的下一篇文章中,我们将介绍如何为 Prime Time 生成代码,这包括允许用户定制代码,以及集成以域或方法甚至更细粒度级别的修改,从而允许重新生成代码。我们还会将它们都绑定到一个插件中,从而展示一种将生成的代码集成到开发过程的方法。