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)erateds*
"/>
<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)ifiables*
"/>
<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-codes*)
"
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-docs*-->s*)
"
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 可选的。一个正则表达式,指定要传递给目标代码的源代码的数量。

关于Eclipse中的开源框架EMF(Eclipse Modeling Framework),第三部分的更多相关文章

  1. 关于Eclipse中的开源框架EMF(Eclipse Modeling Framework)

    Eclipse项目本身可以划分为4个主要的子项目:Equinox,平台,Java开发工具(Java Development Tools,JDT)和插件开发环境(Plug-in Development ...

  2. eclipse中SSH三大框架环境搭建<三>

    相关链接: eclipse中SSH三大框架环境搭建<一> eclipse中SSH三大框架环境搭建<二> 引言:通过上两篇文章我们已经可以掌握struts2和spring的环境的 ...

  3. eclipse中SSH三大框架环境搭建<二>

    通过上一篇博客我们可以轻松搭建strtus2的环境,接下来由我来继续介绍spring的环境搭建以及spring注入的简单使用 相关链接:eclipse中SSH三大k框架环境搭建<一> ec ...

  4. eclipse中SSH三大框架环境搭建<一>

    这里先简单介绍一下我用的三大框架版本以及下载地址 相关链接:eclipse中SSH三大框架环境搭建<二> eclipse中SSH三大框架环境搭建<三> struts-2.3.3 ...

  5. eclipse中配置c++开发环境 Eclipse + CDT + MinGW

    转自eclipse中配置c++开发环境 Eclipse + CDT + MinGW 基本框架:Eclipse + CDT + MinGW 背景知识: CDT:CDT 是完全用 Java 实现的开放源码 ...

  6. 在Eclipse 中下载 开源中国码云上的项目

    功能:使用开源中国代码托管(码云)来托管代码,本地的使用Eclipse,该如何配置? 步骤: 1/  在码云 上建一个工程,(为了访问托管工程的权限) 2/  在eclipse中打开名字叫做“Git ...

  7. Android中使用开源框架android-image-indicator实现图片轮播部署

    之前的博文中有介绍关于图片轮播的实现方式,分别为(含超链接): 1.<Android中使用ViewFlipper实现屏幕切换> 2.<Android中使用ViewPager实现屏幕页 ...

  8. Android中使用开源框架PagerSlidingTabStrip实现导航标题

    此开源框架官网地址:https://github.com/astuetz/PagerSlidingTabStrip 可以理解为配合ViewPager使用的交互式页面指示器控件. 话不多说,先上效果图: ...

  9. 02 eclipse中配置Web项目(含eclipse基本配置和Tomcat的配置)

    eclipse搭建web项目 一.Eclipse基本配置 找到首选项: (一)配置编码 (二)配置字体 (三)配置jdk (四)配置Tomcat 二.Tomcat配置 三.切换视图,检查Tomcat ...

随机推荐

  1. PPTP VPN不能打开百度

    问题: 在阿里云上设置PPTP VPN,电脑能正常连接,能打开京东 淘宝 QQ等没有问题,但是不能打开百度  糯米等网站.开始怀疑是代理设置问题,后面确认未设置独立规则. 1.从应用层看排除特殊规则设 ...

  2. SimpleHttpServer的学习之UML

    如何分析一个稍微大点的源码呢? 静态分析 除了看代码,就是 uml图,UML虽然在书本类与类之间的关系很复杂,可能要一本书,但是最核心的其实很简单: (1)继承 extends (2)实现接口 imp ...

  3. linux tar打包

    范例一:将整个 /etc 目录下的文件全部打包成为 /tmp/etc.tar[root@linux ~]# tar -cvf /tmp/etc.tar /etc <==仅打包,不压缩![root ...

  4. HW7.11

    import java.util.Scanner; public class Solution { public static void main(String[] args) { Scanner i ...

  5. 查找进程对应的PID和对应的端口号

    第一步:首先打开任务管理器.之后左键单击查看,点击下面的选择列. 第二步:之后进入如下界面,把PID勾上.这是我们就可以查看到进程的PID(process id)号了 第三步:首先我们打开DOS窗口, ...

  6. Android问题-selection contains a component,button7,introduced in an ancestor and cannot be deleted.

    问题现象: 在开发Android时增加的控件想删除,可是删除时提示“Android问题-selection contains a component,button7,introduced in an ...

  7. A Tour of Go Methods with pointer receivers

    Methods can be associated with a named type or a pointer to a named type. We just saw two Abs method ...

  8. 转载MSDN 在ASP.NET 中执行 URL 重写

    转载文章原网址 http://msdn.microsoft.com/zh-cn/library/ms972974.aspx 摘要:介绍如何使用 Microsoft ASP.NET 执行动态 URL 重 ...

  9. .NET ORM框架(一)

    最近做项目自己整理了一个ORM框架,分享给大家看看,有很多不足望大家指出. 下面是使用方法 BLL 主要方法 逻辑层:子类继承父类, 直接用BASE调用 ManagerBLL 中的方法. public ...

  10. 【Stage3D学习笔记续】真正的3D世界(六):空间大战

    这就是书上的最终效果了,一个完整的空间大战游戏: 点击查看源码 这里并没有太多的新知识,所涉及的东西更多的是游戏开发方面的优化和技巧,下面我们大家一起来看看: 飞船: 类似粒子效果中的粒子创建方法,我 ...