扩展JDT实现自动代码注释与名目化
副标题#e#
引言
源代码注释是对代码的表明和说明。代码注释可以有效辅佐措施 员筹划未完成的代码任务,淘汰阅读和领略陈旧代码的时间本钱,帮助定位大概 发生错误的代码等,尤其在开拓人员活动较大的环境下,代码注释的良莠直接干系到事情交代的执行效率甚至整个开拓周期的时间和质量节制。清晰的代码编程类型和具体精确的代码注释已经成为评估软件源代码质量的重要参考尺度之一。
Eclipse 作为今朝最优秀的 Java 集成开拓情况之一,固然提供了代码模板 用于定制代码和注释的名目,但它仅仅在第一次成立 Java 文件和自动插入代码 片断时才会按模板定制内容插入预界说的注释和代码片断,这相对付漫长的代码 维护进程是远远不足的。好比:需要为已经存在的所有源代码文件增加一份版权 声明的注释,Eclipse 提供的模版和名目化成果无法满意雷同的需求。本文提供 的东西,正是为补充 Eclipse 模版成果的不敷,使 Java 代码及注释可以在任 何时候更新到最新的模版,极大简化维护代码注释与名目标事情量。
东西先容
本文的办理方案是基于 Eclipse3.4 版本内置的 JDT(Java Development Tool)的基本设施开拓一个插件项目 Add Comment and Format。 此插件凭据 Eclipse 事情空间首选项中代码气势气魄模板的配置为事情空间内的 Java 代码添加、修改注释并名目化 Java 源代码。
读者可下载此插件项 目,将其以已存在项目导入到 Eclipse 中,以“Run an Application”方法运行启动项,建设新的 Eclipse 应用措施。可以看到 新的 Eclipse 应用措施中呈现 Add Comment and Format东西栏按钮(Action) (拜见 图 1)。点击此按钮即可触发添加 Java 代码注释和名目化 Java 代码 事件。
图 1. Add Comment and Format 按钮
下面通过一个实例来展示此东西的执行进程及结果。
首先,从头设置首选项 Code Template的注释名目(拜见 图 2)。在本文示 例中操纵如下:在 Code->New Java File模板中添加 plug in development ,删除 ${filecomment};在 Comments-> Fields模板中将 ${field_type}添 加到字段名前,在 Methods模板中删除 return。
图 2. Eclipse Code Template 首选项设置页面
然后,点击东西栏中 Add Comment and Format按钮(拜见 图 1),执行格 式化代码及注释成果并查察功效(拜见 图 3)。
图 3. 修改完模板前儿女码较量
#p#副标题#e#
图 3为事情空间中某一 Java 文件执行 Add Comment and Format 插件前后 的比拟图。图左为执行之前,Eclipse 事情空间中的代码与模版设定气势气魄纷歧致 ,注释添加东倒西歪,并且已有的注释内容也需要调解。右边为执行之后, Eclipse 凭据模板的设命名目为 Java 文件添加、修改、删除注释并名目化,具 有精采的代码气势气魄。
Add Comment and Format插件可以利便地更新事情空间内 Java 代码气势气魄, 保持代码气势气魄的一致与类型。它既没有改变原有代码的重要构成部门,也没有产 生冗余的代码注释,大大淘汰人工修改的事情量和堕落率。
实现步调:
实现 Add Comment and Format插件的成果主要包罗如下步调:
遍历 Eclipse 事情空间获取 Java 编译单位
从 eclipse 事情空间的资源中得到 Java 项目,而且遍历此项目以得到 Java 编译单位,即事情区内的 Java 源文件(.java)。
获得 Java 编辑单位的事情副本缓存
事情副本是对 Java 源代码举办修改时的缓存,可通过操纵事情副本缓存来 修改代码。
修改代码
通过事情副本缓存修改代码,凭据模板从头生成 Java 代码内容并替换原文 件,从头添加引用包列表,为 Java 代码中要领、字段添加或修改注释内容,并 实时与原文件同步。
名目化代码
挪用 Eclipse JDT 的名目化接口,通过操纵事情副本缓存名目化代码。
生存 Java 源文件
将对事情副本缓存的修改生存到对应的 Java 源文件中。
下面具体接头每个步调。
遍历 Eclipse 事情空间获取 Java 编译单位
从体系布局看,JDT 分为模子和用户界面两部门。模子是 Java 语言类型中 Java 元素的抽象,好比:包、类、要领、字段等等。回收 JDT 提供的 Java 模 型操纵代码,比直接由 Java 源文件中取得和操纵代码的文本越发利便有效,而 且 Java 模子还可以感知其下的文件资源的变革。
Eclipse 事情空间的所有项目资源 (IProject) 可由 ResourcesPlugin 的静 态要领获取,获得事情空间的项目资源之后可以由 JavaCore 提供的静态要领创 建 Java 模子的根元素 IJavaProject(拜见 清单 1)。通过获得 IJavaProject 接口的实例就可以遍历并获得 Java 的所有元素。
下面清单 1 至 4 给出遍历 Java 元素,获取 Java 编译单位的代码。
清单 1. 获得 Java 模子的 IJavaProject 元素
#p#分页标题#e#
// 获得事情空间中的项目资源
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot ().getProjects();
for (IProject project : projects) {
// 按照事情空间资源建设 Java 模子的顶层元素(Java 项目元素)
IJavaProject javaProject = JavaCore.create (project);
……
}
}
清单 1给出获得 Java 模子的 IJavaProject 元素要领。由于 IJavaProject 元素是与资源相关的,即一个 IJavaProject 元素关联到一个 Eclipse 项目资 源,所以在操纵之前需要通过 exits() 要领判定被关联的资源是否存在,以避 免产生异常(拜见 清单 2)。
清单 2. 判定 Java 元素关联的资源是否存在
IJavaProject javaProject = …… ;
// 判定 Java 元素时候存在
if (javaProject.exists() && javaProject != null) {
……
}
包目次包罗源代码文件夹目次,Jar 库以及一些隶属包。对付 Java 项目而 言,可以通过挪用 IJavaProject 类的 getPackageFragmentRoots() 要领获得 的 IPackageFragmentRoot 荟萃。在此荟萃中,第一个元素就是源代码文件夹目 录,因此可直接取其‘ 0 ’元素(拜见 清单 3)。
清单 3. 获得源代码文件夹对应的 Java 元素
IJavaProject javaProject = …… ;
IPackageFragmentRoot root = javaProject.getPackageFragmentRoots()[0];
清单 4是遍历源代码文件夹中的 Java 元素(IPackageFragmentRoot),得 到包(IPackageFragment)中的 Java 编译单位(ICompilationUnit)。
清单 4. 获得编译单位
IPackageFragmentRoot root = ……;
for (IJavaElement pack : root.getChildren()) {
if (pack instanceof IPackageFragment) {
for (ICompilationUnit cu : ((IPackageFragment) pack).getCompilationUnits()) {
// 操纵编译单位(ICompilationUnit)
}
}
}
获得 Java 编辑单位的事情副本缓存
Java 代码的可以通过操纵事情副本举办修改。事情副本是代码举办修改时的 分阶段缓存区域,通过事情副本可以获得操纵代码的缓存。修改代码可直接通过 操纵事情副本的缓存来操纵代码,但必需实时与原文件保持同步,制止后续操纵 与之斗嘴。修改完毕,提交将修改生存在磁盘上。为制止资源挥霍,提交之后丢 弃事情副本。
其道理雷同于常用的 Java 编译器。在编译器中,Java 代码一旦打开,就会 发生一个事情副本,用户生存代码之前的所有操纵均是对事情副本的操纵。只有 举办封锁编辑器或生存代码等提交接码操纵时,才会将文件生存到磁盘上。
本文中东西的实现是通过直接操纵 Java 文件事情副本的缓存来修改 Java 文件的。首先,要按照编译单位得到事情副本,即将编译单位切换到事情副本模 式(拜见 清单 5)。
清单 5. 获得编译单位的事情副本
parentCU.becomeWorkingCopy(new SubProgressMonitor(monitor, 1));
清单 5 中将编译单位切换到事情副本模式,就是在内存中建设一块存放 Java 代码副本的处所,即事情副本缓存。
事情副本模式下,事情副本可以获得事情副本缓存,一个 IBuffer 的实例。 该实例雷同于 StringBuffer 的 API,对其修改就可以到达修改与之关联的 Java 元素的结果。在提交接码之前,对缓存修改一直生存在事情副本中,直至 被显示提交(拜见 清单 6)。
清单 6. 获得事情副本缓存
// 获得事情副本缓存
IBuffer buffer = parentCU.getBuffer();
修改代码
Eclipse 中 Java 代码包括的注释种类及顺序是由 Code >> New Java File模板抉择,注释的详细内容由 Comments下相应的模板抉择。为使事情空间 内的代码具有一致的注释气势气魄,首先应凭据代码构建模板的形式从头构建代码, 处理惩罚是否含有文件注释、类注释或其他的信息;接着,处理惩罚从头构建代码时丢失 的重要信息,如引用包;然后,处理惩罚从头构建代码时未处理惩罚的类体内部代码注释 ,如要领注释和字段注释;最后,将从头构建后的代码名目化。这样,Java 代 码就具有了类型的注释及精采的气势气魄。
从头构建 Java 代码
#p#分页标题#e#
CodeGeneration(org.eclipse.jdt.ui)提供了获取 Code Templates首选项 页面的种种模板信息的重载静态要领,并以字符串的形式返回,如文件注释、类 注释、要领注释、字段注释、新 Java 文件等。开拓人员可扩展其要领获得差异 的模板信息。
在从头构建 Java 文件时,凭据模板名目从头生成代码,并替换原有代码, 最后与原文件举办同步(拜见 清单 7)。
清单 7. 从头构建 Java 代码
// 获得 Java 代码中的类
IType type = parentCU.getTypes()[0];
// 获得类的内容
String typeContent = type.getSource();
// 假如类含有 Javadoc,取类内容的子串,去除注释内容
if (type.getJavadocRange() != null)
typeContent = typeContent.substring(type.getJavadocRange ().getOffset()
+ type.getJavadocRange().getLength() - type.getSourceRange ().getOffset());
// 挪用 CodeGeneration 的得到新 Java 文件的要领,从头构建代 码
String content = CodeGeneration.getCompilationUnitContent (parentCU,
CodeGeneration.getTypeComment(parentCU, type.getElementName (),
lineDelimiter), typeContent, lineDelimiter);
// 用新获得的 Java 代码替换原有代码
buffer.replace(0, parentCU.getSourceRange().getLength(), content);
// 同步
JavaModelUtil.reconcile(parentCU);
清单 7从头构建 Java 代码的步调是挪用 CodeGeneration 的 getCompilationUnitContent() 要领获得新 Java 代码的内容,将其替换事情副 本缓存(IBuffer)中的 Java 代码,再挪用 reconcile() 要领将修改与事情副 本同步。
getCompilationUnitContent() 要领主要成果是读取 Code >> New Java File模板,按照文件内容替换个中的表达式,返回一个具有名目(包括换 行符、空格)的字符串。该要领有 4 个参数,别离为编译单位,类注释内容, 类内容(不包括类注释),项目标行脱离符。
类注释内容由 CodeGeneration 的 getTypeComment() 要领获得,该要领读 取 Comment >> Types模板并以字符串的范例返回。个中, getTypeComment() 要领的第二个参数是类标识符名称,如文件 A.java 的类标 识符名称就是 A。类标识符名称由 IType 的 getElementName() 要领获得。
符号换行的行脱离符可由 StubUtility 的 getLineDelimiterUsed() 要领得 到(拜见 清单 8),获取类注释、要领注释、字段注释模板内容的要领也同样 需要此参数。
清单 8 获得项目行脱离符
String lineDelimiter = StubUtility.getLineDelimiterUsed (javaProject);
个中,类内容参数的处理惩罚最为巨大。利用 IType 的 getSource() 要领获得 的字符串不只包括类声明体的内容,并且包罗类的 Javadoc 注释。而通报给 getCompilationUnitContent() 要领的类内容参数中,不该包罗类注释。因此, 当类存在 javadoc 注释时,需要将其去除。本文利用 String 的 substring() 要领,在 getSource() 获得的字符串中截取类内容。类声明体内容的开始位置 即 javadoc 内容的竣事位置,由于 Itype 的 getJavadocRange() 要领获得类 体最后的一个 javadoc 注释区域范畴,这个范畴相对整个 Java 文件的竣事位 置减去 IType 在 Java 文件中的绝对开始位置就获得此 JavaDoc 在 IType 类 getSource() 要领返回文本中的相对竣事位置。
利用 IBuffer 替换原代码的操纵时,需确定处理惩罚内容的起始位置及长度。 IJavaElement 的 getSourceRange() 要领,可获得 Java 元素的区域范畴,起 始位置 (getOffset() 要领 ) 及长度 (getLength() 要领 )。由于 IJavaElement 是其他 Java 模子元素的父类,因此,Java 模子的元素均可利用 getSourceRange() 要领获得元素的区域范畴,并获得其元素内容的起始位置和 长度,在后续的实现中多次利用此要领确定处理惩罚元素内容的位置及长度。
在操纵进程,对事情副本缓存的修改需要通知原资源,担保原文件与副本的 一致性,不然后续操纵照旧基于原文件举办,会包围之前所做的操纵。 JavaModelUtil 的 reconcile() 要领将触发元素变革事件,担保文件的同步。
从头添加引用包
由于在从头构建 Java 代码时,CodeGeneration 的 getCompilationUnitContent() 要领的实现中没有涉及引用包的处理惩罚。因此,需 要从头为 Java 代码添加引用包。
代码 清单 9获得引用包的列表,以字符串列表的范例返回。
清单 9. 获得引用包列表
public List<String> getImport(ICompilationUnit parentCU,
String lineDelimiter) throws JavaModelException {
List<String> allImports = new ArrayList<String> ();
for (int i = 0; i < parentCU.getImports().length; i++) {
allImports.add(parentCU.getImports()[i].getElementName());
}
return allImports;
}
#p#分页标题#e#
代码 清单 10为 Java 代码从头添加引用包列表。由于代码从头构建且与原 文件同步后,就会完全丢失引用包的信息,需要在 清单 7执行之前得到引用包 列表。代码从头构建之后再利用 createImport() 要领为 Java 文件逐一从头添 加引用包,并实时与原文件同步。清单 10 中的省略号处的内容同清单 7。
清单 10. 从头添加引用包列表
List<String> allImports = getImport(parentCU, lineDelimiter);
……
// add import
for (String name : allImports) {
parentCU.createImport(name, null, monitor);
JavaModelUtil.reconcile(parentCU);
}
处理惩罚 Javadoc
由于代码从头构建时,将类声明体内容作为整体和其他内容从头组合,其内 部并没有修改。因此,需要处理惩罚 Java 代码内部的要领、字段的 javadoc 注释 。获取 Java 代码中所有要领、字段,并识别此范例元素是否含有 Javadoc 注 释,若含有,将此范例元素对应的模板注释内容与原注释替换;不然,为此元素 添加新的模板注释内容。
清单 11通过 IType 获得对要领、字段操纵的工具,并返回 IMenber 范例的 列表。
清单 11. 获得 Java 代码中的 method、field
public List<IMember> getAllMember(IType type) throws JavaModelException {
List<IMember> list = new ArrayList<IMember> ();
// 获得所有要领,并添加到 list 中
for (IMethod method : type.getMethods()) {
list.add(method);
}
// 获得所有字段,并添加到 list 中
for (IField field : type.getFields()) {
list.add(field);
}
return list;
}
清单 12识别元素范例并针对范例获得差异的模板注释;通过 IMember 的 getJavadocRange() 要领,判定是否含有 javadoc,没有则为此元素添加注释; 不然,用从头读取的模板注释替换原有注释内容。
清单 12. 处理惩罚 Javadoc
// 跟据元素范例差异获得差异的注释模板内容
for (IMember member : getAllMember(type)) {
String comment = null;
switch (member.getElementType()) {
// 要领
case IJavaElement.METHOD:
comment = getMethodComment((IMethod) member, lineDelimiter);
break;
// 字段
case IJavaElement.FIELD:
comment = getFiledComment((IField) member, lineDelimiter);
break;
// 其他环境,返回类注释
default:
comment = CodeGeneration.getTypeComment(parentCU,
type.getElementName(), lineDelimiter);
}
// 元素是否含有 Javadoc,没有添加,有则替换
if (member.getJavadocRange() != null)
buffer.replace(member.getJavadocRange().getOffset(), member
.getJavadocRange().getLength(), comment);
else
buffer.replace(member.getSourceRange().getOffset(), 0, comment);
// 同步
JavaModelUtil.reconcile(copyCU);
}
清单 12操作 IJavaElement 的 getElementType() 要领获得范例属性值,判 断与哪种范例常量(IJavaElement.METHOD、IJavaElement.FIELD)匹配,识别 元素范例;若均不匹配,默认此元素范例是类。
getMethodComment()(清单 13)、getFiledComment()(清单 14)要领别离 获得要领、字段的模板内容。
清单 13. 获得要领注释模板内容
public String getMethodComment(IMethod method, String lineDelimiter)
throws CoreException {
IType declaringType = method.getDeclaringType();
IMethod overridden = null;
if (!method.isConstructor()) {
ITypeHierarchy hierarchy = SuperTypeHierarchyCache
.getTypeHierarchy(declaringType);
MethodOverrideTester tester = new MethodOverrideTester(
declaringType, hierarchy);
overridden = tester.findOverriddenMethod(method, true);
}
return CodeGeneration.getMethodComment(method, overridden,
lineDelimiter);
}
#p#分页标题#e#
清单 13是获得要领模板注释的代码片断。按照差异范例的要领(IMethod) 参数,读取差异的模板,并均以字符串范例返回。若是结构要领,读取 Comment >> Constructors中的模板;若是重载要领,读取 Overriding methods中 的模板;若是其他要领,读取 Methods中的模板。if 语句判定要领参数是否为 结构函数,由于结构函数不能被重载,因此不需要在其父类中递归查找。
清单 14. 获得字段注释模板内容
public String getFiledComment(IField field, String lineDelimiter)
throws IllegalArgumentException, CoreException {
String typeName = Signature.toString(field.getTypeSignature ());
String fieldName = field.getElementName();
return CodeGeneration.getFieldComment(field.getCompilationUnit (),
typeName, fieldName, lineDelimiter);
}
清单 14是获得 Comment >> Fields模板的 Javadoc 字符串。通过 IField 得到字段名及字段范例,扩展 JDT 的要领。
名目化代码
代码修改后,为担保统一的排版名目,需举办代码名目化,清单 15先容 Java 代码名目化排版的详细实现。
清单 15. 名目化排版
ICompilationUnit copyCU = ...;
IBuffer buffer = ...;
IType type = ...;
// 类体的范畴
ISourceRange sourceRange = type.getSourceRange();
// 得到源代码的类体内容
String originalContent = buffer.getText(sourceRange.getOffset (),
sourceRange.getLength());
// 代码举办名目化
String formattedContent = CodeFormatterUtil.format(
CodeFormatter.K_CLASS_BODY_DECLARATIONS, originalContent,0,
lineDelimiter, copyCU.getJavaProject());
// 去除 tab 制表符和空格符
formattedContent = Strings.trimLeadingTabsAndSpaces (formattedContent);
// 替换原文件
buffer.replace(sourceRange.getOffset(), sourceRange.getLength (),
formattedContent);
代码名目化仍是针对缓存的操纵,挪用 JDT 提供名目化接口 format() 要领 举办缓存名目化,最终反应到 Java 代码上。format() 要领的第一个参数暗示 名目化的范例,第三个参数是代表缩升级此外整数范例,小于便是 0 的值代表 没有缩进,一级别代表一个 TAB 制表符的缩进度。
生存 Java 源文件
修改完毕,需要将代码的变革提交才气生存到磁盘上。为了资源挥霍,提交 之后,扬弃副本。清单 16是提交并扬弃副本的操纵。
清单 16. 生存文件
IProgressMonitor monitor = ...;
ICompilationUnit copyCU = ...;
// 生存
copyCU.commitWorkingCopy(true, monitor);
if (copyCU != null)
// 扬弃副本
copyCU.discardWorkingCopy();
总之,针对单个编译单位的修改的步调是将事情单位转化为事情副本,获得 事情副本缓存;修改缓存并按时与原文件同步;提交变革、扬弃副原来生存文件 。
竣事语
本文先容了一个为 Eclipse 事情空间中的 Java 代码自动添加统一注释并格 式化排版的东西及其详细实现。按照 Eclipse Code Template 的设置,通过扩 展 Eclipse Java Development Tool(JDT)API,来实现 Java 代码与注释的自 动名目化成果。通过本文,但愿开拓人员可以或许更好地相识和应用 JDT 相关常识 ;通过扩展与设置 Eclipse 成果,让 Eclipse 成为开拓人员越发驾轻就熟的可 设置开拓平台。
原文地点:http://www.ibm.com/developerworks/cn/opensource/os-cn- ecl-cf/