Eclipse 的 JET 技术
JET 技术介绍
Eclipse 的 Java Emitter Templates(JET)一个开源的模板引擎,其功能是在 Eclipse Modeling Framework(EMF)中生成代码。 JET 的语法与 JSP 语法比较相似,但它们处于不同的应用领域。
JET 的模板文件(template files)后缀一般为(*.jet),但为区分生成文件的类型建议扩展名采用生成文件类型加 jet 后缀的方式,如 ***.javajet、***.textjet 等。
org.eclipse.emf.codegen.jet.JETEmitter 是 JET 的核心类,其 generate() 方法完成实现两个功能,将模板文件转换为模板执行类(template implementation class)然后通过模板执行类的输出生成相应的代码或文本。
图 1. 文本生成流程图
JET 应用示例
本文随附三个示例源码,下文描述的三个示例分别对应示例源码中的 demo1、demo2 及 demo3,示例中具体方法写在示例源码中 SampleNewWizard 的 doFinish 方法中。将示例项目以 Eclipse Application 方式运行后,在新运行的 Eclipse 平台上选择菜单 FileNewOther,在弹出的 New 对话框中选择 JET Sample Wizards Demo1 至 Demo3,在弹出对话框中填入相应内容即可显示示例效果。
图 2. JET Sample Wizards
下文用三个示例来演示 JET 的功能和用法。示例 1 演示使用 JETEmitter 类生成简单的文件。示例 2 利用 JET 中的骨架 (skeleton) 技术对模板编译后的模板执行类进行方法定制。示例 3 利用类 JMerger 与合并规则达到自动生成代码与手工代码合并的效果。
示例 1 简单的文件生成(demo1)
1 编写模板,JET 语法与 JSP 语法很相似。下面是一个最简单的带有参数的模板文件 demo1.jet 。
清单 1. Demo1.jet
<%@jet package="demo.translated" imports="java.util.List" class="Demo1"%> Hello, <%=((List)argument).get(0).toString()%>! The current time is <%=new java.util.Date()%>.
模板文件一般放在插件项目的 templates 目录下,模板中的 argument 是 JET 的隐含变量,它代表用户的输入参数 , 一般是数据变量集。
模板的第一行表示生成内容的包路径为 demo.translated,模板执行类是 Demo1.java,该类 import java.util.List 。
第二行和第三行是 JET 需要生成的文件内容,使用 <% …… %> 包括代码,使用 <%= …… %> 打印表达式的值,与 JSP 类似,正确地使用 <% …… %> 标签就可以添加任何逻辑循环或结构,其内容很好理解,但 JET 生成代码中,常量字符串最终要以变量的形式存在,于是其生成内容如下:
清单 2. 生成内容
package demo.translated; import java.util.List; public class Demo1 { protected static String nl; public static synchronized Demo1 create(String lineSeparator) { nl = lineSeparator; Demo1 result = new Demo1(); nl = null; return result; } public final String NL = nl == null ? (System.getProperties(). getProperty("line.separator")) : nl; protected final String TEXT_1 = "Hello, "; protected final String TEXT_2 = "!" + NL + "The current time is "; protected final String TEXT_3 = ". "; protected final String TEXT_4 = NL; public String generate(Object argument) { final StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(TEXT_1); stringBuffer.append(((List)argument).get(0).toString()); stringBuffer.append(TEXT_2); stringBuffer.append(new java.util.Date()); stringBuffer.append(TEXT_3); stringBuffer.append(TEXT_4); return stringBuffer.toString(); } } 调用 JET 执行模板生成操作的代码如下:
清单 3. 生成操作代码
// 模板文件所在插件项目的名称 String pluginId = "jet3"; // 通过插件项目获得其路径(basePath) String basePath = Platform.getBundle(pluginId).getEntry("/").toString(); // 模板文件在项目中的路径及文件名 String uri = "templates/echo.jet"; JETEmitter emitter = new JETEmitter(basePath + uri); IProgressMonitor progress = new NullProgressMonitor(); // 声明一个 List 作为数据变量集的 container List<String> argument = new ArrayList<String>(); argument.add(fileName); argument.add("12334"); // 对当前模板进行转换并将需要输出成实际文件的内容返回以便通过输出流输出 String result = emitter.generate(progress, new Object[] { argument });
示例 2 骨架(skeleton)的使用(demo2)
实际应用中往往会有多个代码执行类调用公用的方法或变量的情况,这时候我们通常会希望能够对代码执行类加入特定的方法,或在代码的生成过程中定制处理。对这类问题 JET 提供了一种强大的解决方案,骨架(skeleton)。
所谓骨架,简单来说是修改编译后的模板文件代码的样子。比如我们希望模板文件编译后的代码执行类要继承某个基类、实现某个接口、包含某个常量或方法,就应采用骨架技术。
以下对示例 1 中的模板进行少许修改,以对骨架有一个基本的了解。
在 templates 目录中,创建 demo.skeleton 文件,编辑其内容为
清单 4. demo.skeleton 文件内容
import java.util.Date; public class CLASS { public String s = "Mission Completed!"; private Date getCurrentDate() { return new Date(); } public String generate(Object argument) { return ""; } }
在该文件中,我们 import java.util.Date 类,声明了 String s 及声明了方法 getCurrentDate(),这些内容将被合并到对模板文件进行编译后的代码中。此外,在文件中的类名“ CLASS ”在代码执行类中会被模板文件中的代码执行类类名替换。
将 demo1.jet 文件修改为以下内容,并将文件另存为 demo2.jet 。
清单 5. demo2.jet
<%@jet package="demo.translated" imports="java.util.List" class="Demo2" skeleton="demo.skeleton"%> Hello, <%=((List)argument).get(0).toString()%>! The current time is <%=getCurrentDate()%>. <%=s%>
经 JET 编译后的模板文件代码内容如下,其中红色文字为利用骨架合并的效果。
清单 6. 模板文件代码
package demo.translated; import java.util.Date; import java.util.List; public class Demo2 { protected static String nl; public static synchronized Demo1 create(String lineSeparator) { nl = lineSeparator; Demo2 result = new Demo2(); nl = null; return result; } public final String NL = nl == null ? (System.getProperties().getProperty("line.separator")) : nl; protected final String TEXT_1 = "Hello, "; protected final String TEXT_2 = "!" + NL + "The current time is "; protected final String TEXT_3 = ". "; protected final String TEXT_4 = NL; protected final String TEXT_5 = NL; public String s = "Mission Completed!"; private Date getCurrentDate() { return new Date(); } public String generate(Object argument) { final StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(TEXT_1); stringBuffer.append(((List)argument).get(0).toString()); stringBuffer.append(TEXT_2); stringBuffer.append(getCurrentDate()); stringBuffer.append(TEXT_3); stringBuffer.append(TEXT_4); stringBuffer.append(s); stringBuffer.append(TEXT_5); return stringBuffer.toString(); } }
在这里有一点需要注意,JET 对模板文件的编译只会将其内容编译到最后一个方法中。因此,骨架最后一个方法必须为 public String generate(Object argument),若将其他方法放在最后,代码执行类在将模板编译后与骨架进行合并时会出错。
示例 3 使用 JMerger 实现代码合并(demo3)
JET 可以根据事先编辑好的模板文件生成代码,并且还支持重复生成。实际项目开发中,有时需要将自动生成的代码进行手工修改,这时如果因某种原则需重新执行代码生成,会导致手工编写的代码丢失。在 JET 中解决此类问题的方法是代码合并技术,它能将自动生成的内容和手工编写的内容区分开,在必要时进行合并,JET 中负责合并代码的类是 JMerger 。本示例讨论利用 JMerger 对代码进行合并的技术。
执行 JET 引擎的程序代码如下。
清单 7. JET 引擎的程序代码
// 模板文件所在插件项目的名称 String pluginId = "demo3"; // 模板文件在项目中的路径及文件名 String uri = Platform.getBundle(pluginId).getEntry("/").toString(); uri += "templates/demo4.jet"; // 声明 JETEmitter JETEmitter emitter = new JETEmitter(uri); IProgressMonitor progress = new NullProgressMonitor(); // 声明一个 Map 作为数据变量集的 container Map argument = new HashMap(); argument.put("fileName", fileName); argument.put("className", fileName.substring(0, fileName.indexOf("."))); // 对当前模板进行转换并将需要输出成实际文件的内容返回以便通过输出流输出 String result = emitter.generate(progress, new Object[] { argument }); // 声明 JMerger JMerger jmerger = new JMerger(); // 合并规则文件在项目中的路径及文件名 String uri2 = Platform.getBundle(pluginId).getEntry("/").toString(); uri2 += "/templates/emf-merge.xml"; // 声明 JControlModel,作为合并规则 JControlModel controlModel = new JControlModel(uri2); jmerger.setControlModel(controlModel); // 在 JMerger 中设置需要合并的源文件内容 jmerger.setSourceCompilationUnit(jmerger .createCompilationUnitForContents(result)); // 在 JMerger 中设置需要合并的目标文件内容 jmerger.setTargetCompilationUnit(jmerger .createCompilationUnitForInputStream(new FileInputStream(file .getLocation().toFile()))); // 对 JMerger 中目标文件与源文件进行内容合并 jmerger.merge(); return new ByteArrayInputStream(jmerger.getTargetCompilationUnit() .getContents().getBytes());
示例的模板内容如下
清单 8. 示例模板内容
<%@jet package="demo.translated" class="Demo3"%> package demo3; public class <%=((java.util.Map)argument).get("className")%> { /** * Target javadoc 11 * Target javadoc 14 * * @generated */ public void printMessage1() { // This is my owner code System.out.print("Will be replace"); System.out.println("Source code 1"); } /** * Target javadoc 21 * Target javadoc 24 * * @generated */ public void printMessage2() { // This is my owner code System.out.print("Will be replace"); System.out.println("Source code 2"); }
生成类加入手工代码后的代码
清单 9. 加入手工代码后的代码
/** * Target javadoc 11 * Target javadoc 12 * Target javadoc 13 * Target javadoc 14 * * @generated NOT this target delete or add a word NOT */ public void printMessage1() { // This is my owner code System.out.print("This is user code"); } /** * Target javadoc 21 * <!-- begin-user-doc --> * Target javadoc 22 * Target javadoc 23 * <!-- end-user-doc --> * Target javadoc 24 * * @generated */ public void printMessage2() { // This is my owner code System.out.print("This code will be replace"); System.out.print("Will be replace"); System.out.println("Source code 2"); }
再次执行 JET,重新生成 XXXX,新的生成代码如下,其中 printMessage1 方法 @generated 被设置为 NOT,该方法的 javadoc 注释及方法体内容都不会被合并,printMessage2 方法 javadoc 注释中加入标签 <!-- begin-user-doc --> 和 <!-- end-user-doc --> 在两个标签中间的间隔部分的注释信息将会合并到自动生成的代码中。
清单 10. 新生成的代码
/** * Target javadoc 11 * Target javadoc 12 * Target javadoc 13 * Target javadoc 14 * * @generated NOT this target delete or add a word NOT */ public void printMessage1() { // This is my owner code System.out.print("This is user code"); } /** * Target javadoc 21 * <!-- begin-user-doc --> * Target javadoc 22 * Target javadoc 23 * <!-- end-user-doc --> * Target javadoc 24 * * @generated */ public void printMessage2() { // This is my owner code System.out.print("This code will be replace"); System.out.print("Will be replace"); System.out.println("Source code 2"); }
对 JMerger 合并规则的描述的文章很多,本文不再累述。本例中使用的合并规则是插件 org.eclipse.emf.codegen.ecore 中的 emf-merge.xml 的内容。将其复制到项目的 templates 文件夹中即可达到效果。
JET 技术总结
Eclipse 中的 JET 技术作为 EMF 不可或缺的技术之一,其优势显而易见,易学、易用、易上手使开发人员可以在很短的时间就可以开始开发工作,其与 JSP 类似的模板语言也降低了模板开发时的门槛。它可以生成开发人员需要的任何格式的文本类型文件,例如 java、xml、sql 等等。此外 , 骨架技术使模板开发人员在针对特定类型模板文件编写时,可以进行单独优化,以减少开发人员的工作量。
JET 支持代码合并功能,通过使用简便的代码和实现定义好的合并规则就可以将自动生成代码与人工手写代码有效的整合。
但所有的事物都是有利有弊的,JET 也不例外。 JET 未提供专用的模板和骨架编辑工具,效率对开发效率造成一定影响。另外 JET 极大程度上依赖 Eclipse 的插件机制,而其模板和骨架在代码运行前无法很简便的得知其生成的模板执行类的内容也是件比较麻烦的事情。
JET 与 M2T
刚刚接触 JET 的开发人员会存在一个困惑,在 Eclipse 关于 EMF 的 org.eclipse.emf.codegen 插件项目中存在一层名称为 jet 的包路径,而 Eclipse 在项目 M2T 中也存在有 org.eclipse.jet 插件项目,它们有什么关系呢?
org.eclipse.jet 一般称为 JET2,其内部很多关键类也都是用 JET2 作为类名的前缀,而 org.eclipse.emf.codegen 一般称为 JET,是 EMF 项目不可缺少的核心,其中很多关键类都是用 JET 作为类名前缀。但是即使是这样,有不少开发人员仍旧对其名称发生困惑,因此,Eclipse 在其网站中对这两者的名称已经正式发布,org.eclipse.emf.codegen 插件定名为 EMF.Codegen,org.eclipse.jet 插件定名为 JET,本文考虑到大多开发人员的描述习惯以及关键词与类名前缀的统一,文章中讨论的 JET 技术是指 Eclipse 的插件 org.eclipse.emf.codegen,也即俗称的 JET 技术,或 Eclipse 中 EMF 的 EMF.Codegen 技术。
Eclipse 的项目生成技术
在前文的描述中,利用 JET 技术中的类 JETEmitter 能够生成 JavaProject 。但实际项目开发的需求往往比这要灵活得多,可能需指定编译器、设置插件引用、设定 jar 包引用等。通过 org.eclipse.jdt.core 中的 IJavaProject 及 org.eclipse.core.resources 中的 Iproject 开发人员可以灵活调整 JavaProject 的属性。。
下一示例演示 JavaProject 的创建及属性修改。该示例将创建名称为 demo 的 JavaProject
图 3. demo 的 JavaProject 项目在 Package Explorer View 中的展示
首先在当前 IWorkspace 中获得 IProject 的实例,并通过 create() 方法在文件系统中创建该 Project 。
清单 11. 生成项目
// 生成的项目的名称 String _projectName = "demo"; IWorkspace workspace = ResourcesPlugin.getWorkspace(); // 从当前 workspace 中获取 IProject 实例,其名称为 demo IProject project = workspace.getRoot().getProject(_projectName); // 创建项目 project.create(null); // 对该项目进行描述,其内容输出到 .project 文件 IProjectDescription description = workspace .newProjectDescription(project.getName()); description.setNatureIds(new String[] { JavaCore.NATURE_ID }); description.setLocation(null); // 在当前 Workspace 环境中打开该项目 project.open(null); // 在项目中设置刚设置的项目描述 project.setDescription(description, null);
通过 JavaCore 将当前项目转换为 JavaProject,并将其原有的 source root 删除,由于 Project 默认的 source root 为项目的根目录而非 JavaProject 使用的项目下的 src 路径,需要对其进行修改,本文由于在后面将会介绍对 source root 的创建,因此在这里采用删除的方式,实际创建时可依据需要进行修改。
清单 12. 生成 JAVA 项目
// 在 JavaCore 中将当前 IProject 创建为 IJavaProject IJavaProject javaProject = JavaCore.create(project); // 获得生成项目中的 classpath 信息,并将项目中的 source 目录信息删除 // 由于在下面将重新生成项目的 source root,因此为了保证不重复, // 在这里将多余的错误 source root 删除 List<IClasspathEntry> classpath = new UniqueEList<IClasspathEntry>( Arrays.asList(javaProject.getRawClasspath())); for (int i = 0, len = classpath.size(); i < len; i++) { IClasspathEntry entry = classpath.get(i); if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE && ("/" + project.getName()).equals(entry.getPath() .toString())) { classpath.remove(i); } }
创建 JavaProject 的 classpath 信息,每个 IClasspathEntry 对应 classpath 中的一条细目,并将其加入到 classpath 中。
清单 13. 生成 Classpath
// 添加新的 classpath 细目,包括 source 目录,bin 目录运行时的 container // 以及其他需要添加的如变量(variable),jar 包(library)等等 // 生成效果为 <classpathentry kind="src" path="src"/> IClasspathEntry classpathEntry = JavaCore.newSourceEntry(new Path("/" + project.getName() + "/src")); // 生成效果为 // <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> IClasspathEntry jreClasspathEntry = JavaCore .newContainerEntry(new Path( "org.eclipse.jdt.launching.JRE_CONTAINER")); classpath.add(classpathEntry); classpath.add(jreClasspathEntry); // 在此处可以创建其他需要的 classpath 信息
通过 IFolder 在文件系统中创建项目需要的 src 文件夹和 bin 文件夹。
清单 14. 生成 SRC 文件夹
// 在 JavaProject 的文件系统中创建文件夹 src IFolder sourceFolder = project.getFolder(new Path("src")); if (!sourceFolder.exists()) { sourceFolder.create(false, true, null); } // 在 JavaProject 的文件系统中创建文件夹 bin IFolder runtimeFolder = project.getFolder(new Path("bin")); if (!runtimeFolder.exists()) { runtimeFolder.create(false, true, null); }
将设置好的 classpath 赋值给 JavaProject,其结果反映到 JavaProject 的 .classpath 文件中,并对 JavaProject 的 outputLocation 赋值,最后在当前 workspace 中打开该 JavaProject 。
清单 15. 设置 Classpath 和 bin 目录
// 将设置好的 classpath 赋值给 JavaProject javaProject.setRawClasspath(classpath .toArray(new IClasspathEntry[classpath.size()]), null); // 设置 JavaProject 的 bin 目录 javaProject.setOutputLocation( new Path("/" + project.getName() + "/bin"), null); // 在当前 Workspace 环境中打开该项目 javaProject.open(null);
以上内容可以创建基本的 JavaProject,如果多个 Project 之间存在引用关以,用 Iworkspace 创建引用关系
清单 16. 设置项目引用
// 获得当前 Eclipse 运行环境中的 IWorkspace 实例 IWorkspace workspace = ResourcesPlugin.getWorkspace(); // 从当前 workspace 中获取 IProject 实例 IProject project = workspace.getRoot().getProject(_projectName);
创建 PluginProject 需要调用到 Eclipse 的 PDE 平台,其生成所需要代码在 org.eclipse.pde.core 中,具体生成方式可参看 org.eclipse.pde.ui 中 NewProjectCreationOperation 的 execute 方法。
开发环境集成技术
模板引擎和项目生成是代码生成工作中最重要的工作,完成这些内容,就已经完成了大部分工作。但实际项目开发没有这种简单,开发人员还会受到几个开发环境有关的问题的困扰,这些问题不但会使整个自动代码生成过程变得无趣,还在一定程序上影响开发人员的生成效率。本文只讨论在 Eclipse 平台下的解决。如果您正在使用的集成开发环境不支持扩展或没有开发的 API,可以考虑将项目转移到 Eclipse 环境。
与配置管理工具集成
企业级大型项目,往往需要可靠稳定的配置管理工具,常用的配置管理工具有 CVS、SVN、ClearCase、SourceSafe 等。 IBM 的 ClearCase 因其出色的能力,往往成为开发大型项目的首选,但 ClearCase 的工作原理与 SVN 有明显不同,它对文件的操作有严格的要求,需将加入版本控制的所有的文件设为只读,只有检出操作后才会改为可写,而且要检出代码也能在 ClearCase 中留下完成的版本控制记录,有利于项目的管理。这样就要求生成代码工具支持 ClearCase 的特性,需要时能够自动检出目标文件。幸运的是 Eclipse 的提供了统一的机制操作文件,并且提供编程接口。
解决上述问题,有两个关键点:
1. 需要找到配置管理工具的类型,在 Eclipse 中所有在 Workspace 中的资源类都会实现 IResource 接口,在其中有 getSessionProperty(QualifiedName key) 方法,该方法可以获得配置工具的类型代码如下
清单 17. 获得配置管理工具对象
// 获得配置工具对象 Object obj = resource.getSessionProperty(TeamPlugin.PROVIDER_PROP_KEY); RepositoryProvider provider = (RepositoryProvider) obj; 当 provider.getID().contains("clearcase") 为真的时候就能确定该资源使用 ClearCase 来管理。
2. 从 ClearCase ChecktOut 资源
FileModificationValidator 可以帮助我们完成 CheckOut 资源,具体代码如下
清单 18. 从配置管理工具 CheckOut 文件
// 从 RepositoryProvider 获得 FileModificationValidator FileModificationValidator validator = provider.getFileModificationValidator2(); // 完成 CheckOut 操作 IStatus status = validator.validateEdit(new IFile[] { (IFile) resource }, null);
当 status.isOK() 为 true 时文件证明文件 CheckOut 成功项目应根据需要显示提示窗口或自动完成检出操作。
使用 Eclipse 的自动编译功能
生成代码之后开发人员可能还需做一些手工操作,如识别文件变化、程序编译、配置类型文件处理等。为提升自动化处理程序,我们需要系统能自动发现文件变化,并自动进行处理。
org.eclipse.core.resources.builders 扩展点提供了以上类型的机制。实现 Builder 扩展点,在 Extension 页中增加 org.eclipse.core.resources.builders 扩展点,并指定其实现类。该实现类必须继承 org.eclipse.core.resources.IncrementalProjectBuilder,过载下列方法
protected abstract IProject[] build(int kind, Map args, IProgressMonitor monitor) throws CoreException;
在项目资源发生变化时 Eclipse 将自动调用上述方法。方法参数请参阅 Eclipse 文档。
这样可以在代码生成之后对文件变化做出自动处理。
使用 Eclipse 的 Error Log 显示生成状态
在 Eclipse 平台中进行代码自动生成时,应尽量使用 Eclipse 提供的控制台机制或问题处理机制,避免使用 Log4j 第三方工具。,一个是将错误输出到 Eclipse 的 Console 中。记录在文件中有多种方式就不在此详述。输出到 Console 中就需要使用 Eclipse 的提供的机制。
要达到这个目的需要调用 Eclipse 的 ILog 接口实现代码如下
清单 19. 使用 ILog 接口
// 获得 Bundle Bundle bundle = InternalPlatform.getDefault().getBundle(pluginid); // 获得 ILog ILog elogger = Platform.getLog(bundle); 得到 ILog 对象后可以记录以下几个级别的错误 //Cancel 级别 elogger.log(new Status(Status.CANCEL, pluginid + "-" + loc, message,throwable)); //Error 级别 elogger.log(new Status(Status.ERROR, pluginid + "-" + loc, message,throwable)); //Info 级别 elogger.log(new Status(Status.INFO, pluginid + "-" + loc, message,throwable));
以此方式可在Eclipse
平台中显示生成状态信息,提高用户体验。
结束语
“自动代码生成技术”已成为项目开发不可或缺的一项技能,它通过自动生成文本的方式,减少开发工作量并防止缺陷产生。一般来说,开发人员仅使用模板引擎生成程序代码和数据文件,但从事大型项目开发时,这还远远不够。基于 Eclipse 开发环境,我们可以自动创建项目、添加依赖项、生成代码文件、编译或处理已生成代码、并与配置管理工作交互,甚至可以在 Eclipse 平台中显示执行进度和信息提示。本文融合笔者多年项目开发经验,涵盖自动代码生成工作的各方面技术。希望本文能对使用 Eclipse 平台的技术人员有所启发,在实践中深度发掘 Eclipse 平台的潜能,而不只是将之视为简单的集成开发环境。