怎样分析 Java 代码以进行修改?
JDT 提供了几个工具来帮助您分析代码。本文有意选择了最简单的 IScanner 接口进行演示,它的作用域也最有限。这个接口属于 JDT 工具箱,可以通过 JDT 的 ToolFactory 类访问它。其 createScanner 方法返回一个扫描程序,该扫描程序会简化对一串 Java 代码作标记的工作。它不处理任何非凡困难的操作,只是对所返回的标记进行简单的解析和分类。例如,它指出下一个标记是 public 要害字,其后的标记是一个标识符,再后面的标记是左圆括号,等等。随后,只有当您希望分析一小段代码(您明确理解想要在这段代码中得到什么)时,这个扫描程序才是合适的。您决不会使用扫描程序分析整个 Java 源代码;因为您会转而使用一些对编译器迷而言十分熟悉的工具:JDT 的抽象语法树(Abstract Syntax Tree,AST)框架。
与简单的扫描程序不同,AST 理解语言元素(它们不再只是“标记”)之间的关系。它可以识别象局部变量、实例变量、表达式以及 if 语句等六十多种不同的语言元素。它将帮助您进行涉及范围广泛的重构,或难以满足对标记进行一对一分类的模糊程度非凡高的重构。要更清楚地了解何时使用扫描程序与何时使用 AST 之间的差别,请考虑清单 10 中的代码。
清单 10. 模糊的变量引用
public class Foo {
int foo = 1;
public int foo(int foo) {
return foo + this.foo;
}
public int getFoo() {
return foo;
}
}
假如作为重构的一部分,您希望查找对实例变量 foo 的引用,那么就会明白一个单纯的解析会使区分本地引用和实例变量引用成为一个难题。AST 创建了完整的分析树,其中表示了 Java 源代码的每个元素并对这些元素进行了区分。在这个特例中,不同的类会考虑“foo”引用的上下文,将“foo”引用表示成 AST 的节点(如 FieldDeclaration、SimpleName 和 ThisEXPression),因此您会很轻松地识别它们。
正如前面提到的,本文将只讨论我们所选择的简单例子。对于比较复杂的修改和分析示例,请参阅参考资料一节。现在,让我们回到我们前面跳过的用省略号表示的代码。这个代码将使用 IScanner 的实例以确定并替换源代码中确定成员可视性的要害字。我们将处理的可视性修饰符是 public、private、protected 和 final。通过采用“蛮力”方法,我们可以简化这个解决方案,即,采用两个步骤就可以完成。首先删除方法特征符中所有的可视性修饰符(或至少扫描查找它们,假如找到,就删除),然后插入所希望的修饰符。非凡地:
假如在方法特征符中找到 public、private 或 protected,就删除它们。
插入所请求的可视性修饰符(对于包可视性的情况,不作任何操作,因为这是缺省操作;即没有任何修饰符)。
final 修饰符很简单。因为所希望的行为就是插入和除去这个修饰符,所以假如它存在,我们除去它;否则就插入它。清单 11 中的代码只显示了一个例子,它无条件地将成员的可视性从 pubilc 改成 private。在与本文相关的解决方案中,您将看到每个操作的公共代码都被移到了抽象超类中。它基本上与下面的代码相同,只不过稍作了整理以避免冗余。
清单 11. 扫描是否有 pubilc 要害字
ICompilationUnit cu = member.getCompilationUnit();
if (cu.isWorkingCopy()) {
IBuffer buffer = cu.getBuffer();
IScanner scanner =
ToolFactory.createScanner(false, false, false, false);
scanner.setSource(buffer.getCharacters());
ISourceRange sr = member.getSourceRange();
scanner.resetTo(
sr.getOffset(),
sr.getOffset() + sr.getLength() - 1);
int token = scanner.getNextToken();
while (token != ITerminalSymbols.TokenNameEOF
&& token != ITerminalSymbols.TokenNameLPAREN)
token = scanner.getNextToken();
if (token == ITerminalSymbols.TokenNamePUBLIC) {
buffer.replace(
scanner.getCurrentTokenStartPosition(),
scanner.getCurrentTokenEndPosition(),
scanner.getCurrentTokenStartPosition() + 1,
"private");
break;
}
}
cu.reconcile();
}
注:ITerminalSymbols 定义了扫描程序可以返回的标记名称,它们对应于 Java 语法的标准标记。您可以进一步查询扫描程序以询问当前标记在缓冲区中开始和结束的具体位置,它出现在哪一行上,当然还有标记本身(非凡是象 ITerminalSymbols.TokenNameStringLiteral 和 ITerminalSymbols.TokenNameIdentifier 这样的例子,它们不是保留的要害字)。
上述代码片段中,向 scanner.setSource 方法提供了编译单元的完整源代码,即 Java 源文件中的所有内容。正如前面提到的,扫描程序并不非常适合于大型分析,所以我们必须将它限制用于只有以目标方法的第一个字符开始,一直到调用 setSourceRange 方法作为结束的那部分源代码。IMember 接口继续了 ISourceReference,ISourceReference 是一个答应您查询包含编译单元内的源代码字符串和源代码位置的接口。这使我们不必确定目标方法在 Java 源代码内开始和结束的位置。原本可以用 AST 实现这一点,而 ISourceReference 接口使 AST 成了多余的工具。由于 Java 方法特征符易于解析,所以 IScanner 接口的解析能力和它很匹配。我们必须做的就是查找 public 要害字,它出现在方法声明的前一个字符之后,参数声明的左圆括号之前,用 private 要害字替换它。当然,在该解决方案中,这个接口将处理所有的可能情况,不管该方法最初是 public、private、protected 还是 package(缺省)。
下一步是什么?
本文设定的目标是向您提供一个对 Eclipse 的 Java 开发环境颇具价值的扩展,这样的扩展增强了这个开发环境的生产率。坦率地说,出于简洁性考虑,我多次跳过了一些细节。该解决方案本身就作了一些简化假设,象只答应在编辑器中对已打开的 Java 源代码进行修改。您可能希望在更完整的实现中取消这个限制。
虽然如此,但我还是希望您能感受到什么是可能的,并确信这样做不是非凡困难。本文中我们讨论的是 The Java Developer's Guide to Eclipse 一书某一高级章节的部分内容。该书中有十一个比较浅显的章节讨论了插件开发的基础。象本文一样,大多数章节都包含了一个已文档化的工作解决方案,它可以强化您所学到的知识,大多数内容是以本文中您已看到的相同风格编写的(不过可能没有以这么快的节奏进行讨论!)。
重要:您可能需要向工作空间添加必要的插件,这样解决方案才能编译和运行。选择 Window Preferences Plug-in Development Target Platform,然后选择 Not in Workspace。这将确保解决方案所依靠的基础插件在导入和重新编译过程中可用。
一旦导入完成,您可能需要切换至 Plug-in Development 透视图,在 com.ibm.lab.soln.jdt.excerpt 项目中选择 plugin.XML,然后选择 Update Classpath。这将修改由于 Eclipse 安装路径和解决方案的安装路径不同所引起的编译错误。