源代码注释是对代码的解释和说明。代码注释可以有效帮助程序员规划未完成的代码任务,减少阅读和理解陈旧代码的时间成本,辅助定位可能产生错误的代码等,尤其在开发人员流动较大的情况下,代码注释的良莠直接关系到工作交接的执行效率甚至整个开发周期的时间和质量控制。清晰的代码编程规范和详细准确的代码注释已经成为评估软件源代码质量的重要参考标准之一。
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为工作空间中某一 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 源文件中。
下面详细讨论每个步骤。
从体系结构看,JDT 分为模型和用户界面两部分。模型是 Java 语言规范中 Java 元素的抽象,比如:包、类、方法、字段等等。采用 JDT 提供的 Java 模型操作代码,比直接由 Java 源文件中取得和操作代码的文本更加方便有效,而且 Java 模型还可以感知其下的文件资源的变化。
Eclipse 工作空间的所有项目资源 (IProject) 可由 ResourcesPlugin 的静态方法获取,得到工作空间的项目资源之后可以由 JavaCore 提供的静态方法创建 Java 模型的根元素 IJavaProject(参见 清单 1)。通过得到 IJavaProject 接口的实例就可以遍历并得到 Java 的所有元素。
下面清单 1 至 4 给出遍历 Java 元素,获取 Java 编译单元的代码。
清单 1. 得到 Java 模型的 IJavaProject 元素
|
清单 1给出得到 Java 模型的 IJavaProject 元素方法。由于 IJavaProject 元素是与资源相关的,即一个 IJavaProject 元素关联到一个 Eclipse 项目资源,所以在操作之前需要通过 exits() 方法判断被关联的资源是否存在,以避免发生异常(参见 清单 2)。
|
包目录包括源代码文件夹目录,Jar 库以及一些附属包。对于 Java 项目而言,可以通过调用 IJavaProject 类的 getPackageFragmentRoots() 方法得到的 IPackageFragmentRoot 集合。在此集合中,第一个元素就是源代码文件夹目录,因此可直接取其‘ 0 ’元素(参见 清单 3)。
|
清单 4是遍历源代码文件夹中的 Java 元素(IPackageFragmentRoot),得到包(IPackageFragment)中的 Java 编译单元(ICompilationUnit)。
|
Java 代码的可以通过操作工作副本进行修改。工作副本是代码进行修改时的分阶段缓存区域,通过工作副本可以得到操作代码的缓存。修改代码可直接通过操作工作副本的缓存来操作代码,但必须及时与原文件保持同步,避免后续操作与之冲突。修改完毕,提交将修改保存在磁盘上。为避免资源浪费,提交之后丢弃工作副本。
其原理类似于常用的 Java 编译器。在编译器中,Java 代码一旦打开,就会产生一个工作副本,用户保存代码之前的所有操作均是对工作副本的操作。只有进行关闭编辑器或保存代码等提交代码操作时,才会将文件保存到磁盘上。
本文中工具的实现是通过直接操作 Java 文件工作副本的缓存来修改 Java 文件的。首先,要根据编译单元获得工作副本,即将编译单元切换到工作副本模式(参见 清单 5)。
|
清单 5 中将编译单元切换到工作副本模式,就是在内存中创建一块存放 Java 代码副本的地方,即工作副本缓存。
工作副本模式下,工作副本可以得到工作副本缓存,一个 IBuffer 的实例。该实例类似于 StringBuffer 的 API,对其修改就可以达到修改与之关联的 Java 元素的效果。在提交代码之前,对缓存修改一直保存在工作副本中,直至被显示提交(参见 清单 6)。
|
Eclipse 中 Java 代码包含的注释种类及顺序是由 Code >> New Java File模板决定,注释的具体内容由 Comments下相应的模板决定。为使工作空间内的代码具有一致的注释风格,首先应按照代码构建模板的形式重新构建代码,处理是否含有文件注释、类注释或其他的信息;接着,处理重新构建代码时丢失的重要信息,如引用包;然后,处理重新构建代码时未处理的类体内部代码注释,如方法注释和字段注释;最后,将重新构建后的代码格式化。这样,Java 代码就具有了规范的注释及良好的风格。
重新构建 Java 代码
CodeGeneration(org.eclipse.jdt.ui)提供了获取 Code Templates首选项页面的各类模板信息的重载静态方法,并以字符串的形式返回,如文件注释、类注释、方法注释、字段注释、新 Java 文件等。开发人员可扩展其方法得到不同的模板信息。
在重新构建 Java 文件时,按照模板格式重新生成代码,并替换原有代码,最后与原文件进行同步(参见 清单 7)。
[java] view plain copy
- // 得到 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),获取类注释、方法注释、字段注释模板内容的方法也同样需要此参数。
|
其中,类内容参数的处理最为复杂。使用 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得到引用包的列表,以字符串列表的类型返回。
[java] view plain copy
- 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;
- }
代码 清单 10为 Java 代码重新添加引用包列表。由于代码重新构建且与原文件同步后,就会完全丢失引用包的信息,需要在 清单 7执行之前获得引用包列表。代码重新构建之后再使用 createImport() 方法为 Java 文件逐一重新添加引用包,并及时与原文件同步。清单 10 中的省略号处的内容同清单 7。
[java] view plain copy
- 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
[java] view plain copy
- 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,没有则为此元素添加注释;否则,用重新读取的模板注释替换原有注释内容。
[java] view plain copy
- // 跟据元素类型不同得到不同的注释模板内容
- 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)方法分别得到方法、字段的模板内容。
[java] view plain copy
- 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);
- }
清单 13是得到方法模板注释的代码片段。根据不同类型的方法(IMethod)参数,读取不同的模板,并均以字符串类型返回。若是构造方法,读取 Comment >> Constructors中的模板;若是重载方法,读取 Overriding methods中的模板;若是其他方法,读取 Methods中的模板。if 语句判断方法参数是否为构造函数,由于构造函数不能被重载,因此不需要在其父类中递归查找。
[java] view plain copy
- 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 代码格式化排版的具体实现。
[java] view plain copy
- 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 制表符的缩进度。
修改完毕,需要将代码的变化提交才能保存到磁盘上。为了资源浪费,提交之后,丢弃副本。清单 16是提交并丢弃副本的操作。
[java] view plain copy
- IProgressMonitor monitor = ...;
- ICompilationUnit copyCU = ...;
- // 保存
- copyCU.commitWorkingCopy(true, monitor);
- if (copyCU != null)
- // 丢弃副本
- copyCU.discardWorkingCopy();
总之,针对单个编译单元的修改的步骤是将工作单元转化为工作副本,得到工作副本缓存;修改缓存并定时与原文件同步;提交变化、丢弃副本来保存文件。