使用 Eclipse 的 JMerge 定制生成的代码和编辑器 
Adrian Powell 资深软件开发人员, IBM 2004 年 6 月  
Eclipse Modeling Framework(EMF)中包含了一个开放源代码的工具 JMerge,这个工具可以使代码生成更加灵活,可定制性更好。本文使用一个例子来展示如何将 JMerge 添加到一个应用程序中,并为不同的环境定制 JMerge 的行为。 
概述 本系列文章的 前一篇 介绍了有关 Eclipse 的 Java Emitter Templates (JET)和代码生成的知识,在那篇文章中,您已经看到如何通过使用模板和代码生成器来节省时间,并实现模式级的代码重用。然而在大部分情况中,这都还不够。您需要能够将所生成的代码插入现有的代码中,或者允许以后的开发人员来定制所生成的代码,而不需要在重新生成代码时重新编写任何内容。理想情况下,代码生成器的创建者希望可以支持今后开发人员所有的需求:从修改方法的实现、修改各种方法签名,到修改所生成类的继承结构。这是一个非常有趣的问题,目前还没有很好的通用解决方案;但是有一个很好的纯 Java 的解决方案,称为 JMerge。 
JMerge 是 EMF 中包含的一个开放源代码的工具,可以让您定制所生成的模型和编辑器,而重新生成的代码不会损坏已经修改过的内容。如果描述了如何将新生成的代码合并到现有定制过的代码中,那么 JETEmitter 就可以支持 JMerge。本文通过一个例子来展示其中的一些可用选项。 
第一步 假设您已经添加了一个新项目,在这个项目中需要为编写的每个类都创建一个 JUnit 测试类,这样必须要对编写的每个方法都进行测试。作为一个认真且高效的(或者比较懒的)程序员来说,您决定要编写一个插件,它接受一个 Java 类作为输入,并生成 JUnit 测试例子的存根(stub)。您热情高涨地创建了 JET 和插件, 现在想允许用户定制所生成的测试类;然而在原有类的接口发生变化时,仍然需要重新生成代码。要实现这种功能,可以使用 JMerge。 
从插件中调用 JMerge 的代码非常简单(参见清单 1)。这会创建一个新的 JMerger 实例,以及一个 URI merge.xml,设置要合并的来源和目标,并调用 merger.merge()。然后合并的内容就可以展开为 merger.getTargetCompilationUnit()。 清单 1. 调用 JMerge 
	// ...
	JMerger merger = getJMerger();
	
	// set source
	merger.setSourceCompilationUnit(
		merger.createCompilationUnitForContents(generated));
	
	// set target
	merger.setTargetCompilationUnit(
		merger.createCompilationUnitForInputStream( 
			new FileInputStream(target.getLocation().toFile())));
	
	// merge source and target
	merger.merge();
	// extract merged contents
	InputStream mergedContents = new ByteArrayInputStream(
		merger.getTargetCompilationUnit().getContents().getBytes());
		
	// overwrite the target with the merged contents
	target.setContents(mergedContents, true, false, monitor);
	// ...
// ...
private JMerger getJMerger() {
	// build URI for merge document
	String uri = 
	   Platform.getPlugin(PLUGIN_ID).getDescriptor().getInstallURL().toString();
	uri += "templates/merge.xml";
		
	JMerger jmerger = new JMerger();
	JControlModel controlModel = new JControlModel( uri );
	jmerger.setControlModel( controlModel );
	return jmerger;
}
  |   
要启动这个过程,可以使用清单 2 这个简单的 merge.xml。其中声明了 <merge> 标签,以及缺省的命名空间声明。这段代码最主要的部分在 merge:pull 元素中。此处,源类中每个方法的代码都会被替换为目标类的对应方法的代码。如果一个方法在目标类不存在,就会被创建。如果一个方法只在源类中存在,而在目标类不存在,就会被保留。 清单 2. 一个非常简单的 merge.xml 
<?xml version="1.0" encoding="UTF-8"?>
<merge:options xmlns:merge=
   "http://www.eclipse.org/org/eclipse/emf/codegen/jmerge/Options">
    <merge:pull 
      sourceGet="Method/getBody"
      targetPut="Method/setBody"/>
</merge:options>
  |   
区分生成的方法 这种简单的方法有一个非常明显的问题:每次修改源类并重新生成代码时,此前所做的修改就全部丢失了。我们需要增加某种机制来告诉 JMerge 有些方法已经被修改过了,因此这些方法不应该被重写。要实现这种功能,可以使用 <merge:dictionaryPattern> 元素。 merge:dictionaryPattern 允许您使用正则表达式来区分 Java 元素(参见清单 3)。 清单 3. 一个简单的 dictionaryPattern 
<merge:dictionaryPattern
   name="generatedMember" 
   select="Member/getComment" 
   match="\s*@\s*(gen)erated\s*\n"/>
<merge:pull 
   targetMarkup="^gen$"
   sourceGet="Method/getBody"
   targetPut="Method/setBody"/>
  |   
dictionaryPattern 定义了一个正则表达式,它可以匹配注释中包含 "@generated" 的成员。 select 属性列出了要对这个成员的哪些部分与在 match 属性中给出的正则表达式进行比较。dictionaryPattern 是由字符串 gen 定义的,它就是 match 属性值中圆括号中的内容。 
merge:pull 元素多了一个附加属性 targetMarkup。这个属性可以匹配 dictionaryPattern,它必须在应用合并规则之前对目标代码进行匹配。此处,我们正在检查的是目标代码,而不是源代码,因此用户可以定制这些代码。当用户删除注释中的 "@generated" 标签时, dictionaryPattern 就不会与目标代码匹配,因此就不会合并这个方法体。请参见清单 4。 清单 4. 定制代码 
/**
 * test case for getName
 * @generated
 */
public void testSimpleGetName() {
	// because of the @generated tag,
	// any code in this method will be overridden
}
/**
 * test case for getName
 */
public void testSimpleSetName() {
	// code in this method will not be regenerated
}
  |   
您或许会注意到有些元素是不能定制的,任何试图定制这些代码的企图都应该被制止。为了支持这种功能,要定义另外一个 dictionaryPattern,它负责在源代码(而不是目标代码)中查找其他标记,例如 @unmodifiable。然后再定义一条 pull 规则,来检查 sourceMarkup,而不是 targetMarkup,这样就能防止用户删除标签或阻碍合并操作。请参见清单5。 清单 5. 不可修改代码的 merge.xml 
<merge:dictionaryPattern
   name="generatedUnmodifiableMembers" 
   select="Member/getComment" 
   match="\s*@\s*(unmod)ifiable\s*\n"/>
<merge:pull 
   sourceMarkup="^unmod$"
   sourceGet="Member/getBody"
   targetPut="Member/setBody"/>
  |   
细粒度的定制 在使用这种解决方案一段时间之后,您将注意到有些方法在定制的代码中具有一些通用的不可修改的代码(例如跟踪和日志记录代码)。此时我们既不希望禁止生成代码,也不希望全部生成整个方法的代码,而是希望能够让用户定制一部分代码。 
要实现这种功能,可以将前面的 pull 目标替用清单 6 来代替。 清单 6. 细粒度的定制代码 
<!-- if target is generated, transfer -->
<!-- change to sourceMarkup if the source is the standard -->
<merge:pull 
   targetMarkup="^gen$"
   sourceGet="Method/getBody"
   sourceTransfer="(\s*//\s*begin-user-code.*?//\s*end-user-code\s*)\n"
   targetPut="Method/setBody"/>
  |   
这样会只重写字符串 "// begin-user-code" 之前和 "// end user-code" 之后的内容,因此就可以在定制代码中保留二者之间的内容。在上面的正则表达式中, "?" 表示在目标代码中,除了要替换的内容之外,其他内容全部保留。您可以实现与 JavaDoc 注释类似的功能,这样就可以拷贝注释,同时为用户定制预留了空间。请参见清单 7。 清单 7. 细粒度的 JavaDoc 定制 
<!-- copy comments except between the begin-user-doc
     and end-user-doc tags -->
<merge:pull 
  sourceMarkup="^gen$"
  sourceGet="Member/getComment"
  sourceTransfer="(\s*<!--\s*begin-user-doc.*?end-user-doc\s*-->\s*)\n"
  targetMarkup="^gen$"
  targetPut="Member/setComment"/>
  |   
要支持这种注释,首先要修改开始标签和结束标签,使其遵循 HTML 注释语法,这样它们就不会出现在所生成的 JavaDoc 中;然后修改 sourceGet 和 targetPut 属性,以便使用 "Member/getComment" 和 "Member/setComment"。 JMerge 允许您在细粒度级别上存取 Java 代码的不同部分。(更多内容请参见 附录 A)。 
下一步 到现在为止,我们已经介绍了如何转换方法体,但是 JMerge 还可以处理域、初始化、异常、返回值、import 语句以及其他 Java 元素。它们也采用类似的基本思想,可能只需稍加修改即可。参考 plugins/org.eclipse.emf.codegen_1.1.0/test/merge.xml 就可以知道如何使用这些功能(我使用的是 Eclipse 2.1,因此如果您使用的是其他版本的 Eclipse,那么 ecore 插件的版本可能会不同)。这个例子非常简单,其中并没有使用 sourceTransfer 标记,但是该例显示了处理异常、标志和其他 Java 元素的方法。 
更复杂的例子请参见 EMF 使用 JMerge 的方法: plugins/org.eclipse.emf.codegen.ecore_1.1.0/templates/emf-merge.xml。从这个例子中可以看出 EMF 只允许部分定制 JavaDoc,但是采用上面介绍的一些技巧,就可以为方法体添加支持(这样可以增强 JET 的功能)。 
附录 A:有效的目标选项 在 dictionaryPattern 和 pull 规则中,我们已经使用了 "Member/getComment" 和 "Member/getBody" 以及它们的 setter 方法,但是还有很多其他可用的选项。JMerge 支持 org.eclipse.jdt.core.jdom.IDOM* 中定义的任何类的匹配和取代。所有可用的选项如表 1 所示。 
表 1. 有效的目标选项 
 
| 类型 | 
方法 | 
注释 |  
| CompilationUnit | 
getHeader/setHeader |  
| getName/setName |  
| Field | 
getInitializer/setInitializer | 
不包含 "=" |  
| getName/setName | 
变量名 |  
| getName/setName | 
类名 |  
| Import | 
getName/setName | 
要么是一个完全限定的类型名,要么是一个随需应变的包 |  
| Initializer | 
getName/setName | 
 |  
| getBody/setBody | 
 |  
| Member | 
getComment/setComment | 
 |  
| getFlags/setFlags | 
例如: abstract, final, native 等。 |  
| Method | 
addException | 
 |  
| addParameter | 
 |  
| getBody/setBody | 
 |  
| getName/setName | 
 |  
| getParameterNames/setParameterNames | 
 |  
| getParameterTypes/setParameterTypes | 
 |  
| getReturnType/setReturnType | 
 |  
| Package | 
getName/setName | 
 |  
| Type | 
addSuperInterface | 
 |  
| getName/setName | 
 |  
| getSuperclass/setSuperclass | 
 |  
| getSuperInterfaces/setSuperInterfaces | 
 |   
附录 B:merge:pull 属性 表2 给出了 merge:pull 元素的属性。 
表 2. merge:pull 属性 
 
| 属性 | 
条件 |  
| sourceGet | 
必需的。该值必须是 附录 A 中列出的一个选项,例如 "Member/getBody"。 |  
| targetPut | 
必需的。该值必须是 附录 A 中列出的一个选项,例如 "Member/setBody"。 |  
| sourceMarkup | 
可选的。用来在触发 merge:pull 规则之前过滤必须匹配源代码的 dictionaryPatterns 。格式如 "^dictionaryName$",也可以使用 "|" 将多个 dictionaryPatterns 合并在一行中。 |  
| targetMarkup | 
可选的。用来在触发 merge:pull 规则之前过滤必须匹配目标代码的 dictionaryPatterns。格式如 "^dictionaryName$",也可以使用 "|" 将多个 dictionaryPatterns 合并在一行中。 |  
| sourceTransfer | 
可选的。一个正则表达式,指定要传递给目标代码的源代码的数量。 |   
参考资料 
 
关于作者 Adrian Powell 从刚开始加入 VisualAge for Java Enterprise Tooling 小组开始使用 IBM 的 Java 开发工具,在这里他花费了两年的时间来手工编写一个代码生成器。从那以后,他一直从事 Eclipse 和 VisualAge for Java 中的工具和插件的开发,他现在几乎为 Eclipse 和 VisualAge for Java 的每一个版本都开发了这种工具和插件。Adrian 目前在 IBM 的 Vancouver Centre 从事于电子商务创新方面的研究,在这里他阅读源代码,为同事提供一些支持。 |    |