使用EMF进行建模,第3部分:使用Eclipse的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:pull 元素中。此处,源类中每个方法的代码都会被替换为目标类的对应方法的代码。如果一个 方法在目标类不存在,就会被创建。如果一个方法只在源类中存在,而在目标类不存在, 就会被保留。

清单 2. 一个非常简单的 merge.xml

  

区分 生成的方法

这种简单的方法有一个非常明显的问题:每次修改源类并重新生成代 码时,此前所做的修改就全部丢失了。我们需要增加某种机制来告诉 JMerge 有些方法已 经被修改过了,因此这些方法不应该被重写。要实现这种功能,可以使用 元素。merge:dictionaryPattern 允许您使用正则表 达式来区分 Java 元素(参见清单 3)。

清单 3. 一个简单的 dictionaryPattern

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

细粒度的定制

在使用这种解决 方案一段时间之后,您将注意到有些方法在定制的代码中具有一些通用的不可修改的代码 (例如跟踪和日志记录代码)。此时我们既不希望禁止生成代码,也不希望全部生成整个 方法的代码,而是希望能够让用户定制一部分代码。

要实现这种功能,可以将前 面的 pull 目标替用清单 6 来代替。

清单 6. 细粒度的定制代码

这样会只重写字符串 ” // begin- user-code ” 之前和 ” // end user-code ” 之后的内容,因此就可以在定制代码中保留 二者之间的内容。在上面的正则表达式中, “?” 表示在目标代码中,除了要替换的内容 之外,其他内容全部保留。您可以实现与 JavaDoc 注释类似的功能,这样就可以拷贝注 释,同时为用户定制预留了空间。请参见清单 7。

清单 7. 细粒度的 JavaDoc 定 制

<merge:pull  sourceMarkup="^gen$" sourceGet="Member/getComment"  sourceTransfer="(/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 getNa me/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 可选的。一个正则表达式,指定要传递给 目标代码的源代码的数量。

本文配套源码

不论你在什么时候结束,重要的是结束之後就不要悔恨

使用EMF进行建模,第3部分:使用Eclipse的JMerge定制生成的代码

相关文章:

你感兴趣的文章:

标签云: