处理 Java 代码

插件可以使用 JDT API 来创建类或接口、将方法添加到现有类型中或者改变方法的类型。

改变 Java 对象最简单的方法就是使用 Java 元素 API。可以使用更常见的技术来处理 Java 元素的原始源代码。

使用 Java 元素来修改代码

生成编译单元

通过程序来生成编译单元最简单的方法是使用 IPackageFragment.createCompilationUnit。指定编译单元的名称和内容。于是在包中创建了编译单元,并返回新的 ICompilationUnit

通常,可以通过在对应包目录的相应文件夹中创建扩展名为“.java”的文件资源来按类别创建编译单元。使用类属资源 API 对于 Java 工具是一种旁门左道,因此,在通知类属资源更改侦听器以及 JDT 侦听器将 Java 模型更新为新编译单元之前,Java 模型不会更新。

修改编译单元

大部分对 Java 源代码的简单修改可以使用 Java 元素 API 来完成。

例如,可以通过编译单元查询类型。一旦您具有 IType,就可以使用诸如 createFieldcreateInitializercreateMethodcreateType 的协议来将源代码成员添加至类型。在这些方法中提供了源代码以及关于成员的位置的信息。

ISourceManipulation 接口定义 Java 元素的常见源代码处理。这包括用于重命名、移动、复制或删除类型的成员的方法。

工作副本

可以通过处理编译单元来修改代码(于是修改了底层 IFile)或人们可以修改编译单元的内存副本(称为工作副本)。

工作副本是使用 getWorkingCopy 方法从编译单元中获取的。(注意,要创建工作副本,编译单元不需要存在于 Java 模型中。)当不再需要这种工作副本时,创建这种工作副本的人员应负责使用 destroy 方法来毁坏它。

工作副本修改内存缓冲区。getWorkingCopy() 方法创建缺省缓冲区,但是客户机可以使用 getWorkingCopy(IProgressMonitor, IBufferFactory, IProblemRequestor) 方法提供它们自己的缓冲区实现。客户机可以直接处理此缓冲区的文本。如果客户机直接处理文本,则它们必须经常使用 reconcile() 方法或 reconcile(boolean,IProgressMonitor) 方法来使工作副本与缓冲区同步。

最后,可以使用 commit 方法将工作副本保存至磁盘(用来替换原始的编译单元)。  

例如,下列代码片段使用定制缓冲区工厂在编译单元上创建工作副本。该代码片段将修改缓冲区、核对更改、将更改提交到磁盘,最后毁坏工作副本。

    // Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Get buffer factory
    IBufferFactory factory = ...;
    
    // Create working copy
    IWorkingCopy workingCopy = (IWorkingCopy)originalUnit.getWorkingCopy(null, factory, null);
    
    // Modify buffer and reconcile
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile();
    
    // Commit changes
    workingCopy.commit(false, null);
    
    // Destroy working copy
    workingCopy.destroy();

工作副本还可以供若干客户机共享。共享的工作副本是使用 getSharedWorkingCopy 方法创建的,以后,可以使用 findSharedWorkingCopy 方法来检索它。因此,对于原始编译单元和缓冲区 factory,共享工作副本很重要。

下列内容说明了客户机 1 如何创建共享的工作副本、客户机 2 如何检索此工作副本、客户机 1 如何毁坏此工作副本以及尝试检索共享的工作副本的客户机 2 如何注意到该工作副本不再存在:

    // Client 1 & 2: Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Get buffer factory
    IBufferFactory factory = ...;
    
    // Client 1: Create shared working copy
    IWorkingCopy workingCopyForClient1 = (IWorkingCopy)originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Client 2: Retrieve shared working copy
    IWorkingCopy workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
     
    // This is the same working copy
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Destroy shared working copy
    workingCopyForClient1.destroy();
    
    // Client 2: Attempt to retrieve shared working copy and find out it's null
    workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
    assert workingCopyForClient2 == null;

使用 DOM/AST API 来修改代码

有三种方法来创建 CompilationUnit。第一种方法是使用现有编译单元。第二种方法是使用现有的类文件。第三种方法是从头开始对 AST(抽象语法树)使用工厂方法。

从现有编译单元来创建 AST

这是通过对 AST 使用分析方法来实现的: 所有这些方法都将正确地设置获得的树中每个节点的位置。在创建树之前,必须请求绑定的解决方案。解决绑定是一个成本很高的操作,仅当需要时才执行。一旦修改了树,就会丢失所有位置和绑定。

从现有的类文件创建 AST

这是通过对 AST 使用分析方法实现的: 此方法将正确地设置结果树中的每个节点的位置。在创建树之前,必须请求绑定的解决方案。解决绑定是一个成本很高的操作,仅当需要时才执行。一旦修改了树,就会丢失所有位置和绑定。

从头开始

可以通过对 AST 使用工厂方法来从头创建 CompilationUnit。这些方法名以 new... 开头。以下是创建 HelloWorld 类的一个示例。

第一个代码片段是生成的输出:

	package example;
   import java.util.*;
   public class HelloWorld {
      public static void main(String[] args) {
			System.out.println("Hello" + " world");
		}
	}

下列代码片段是生成输出的相应代码。

		AST ast = new AST();
		CompilationUnit unit = ast.newCompilationUnit();
		PackageDeclaration packageDeclaration = ast.newPackageDeclaration();
		packageDeclaration.setName(ast.newSimpleName("example"));
		unit.setPackage(packageDeclaration);
		ImportDeclaration importDeclaration = ast.newImportDeclaration();
		QualifiedName name =
			ast.newQualifiedName(
				ast.newSimpleName("java"),
				ast.newSimpleName("util"));
		importDeclaration.setName(name);
		importDeclaration.setOnDemand(true);
		unit.imports().add(importDeclaration);
		TypeDeclaration type = ast.newTypeDeclaration();
		type.setInterface(false);
		type.setModifiers(Modifier.PUBLIC);
		type.setName(ast.newSimpleName("HelloWorld"));
		MethodDeclaration methodDeclaration = ast.newMethodDeclaration();
		methodDeclaration.setConstructor(false);
		methodDeclaration.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
		methodDeclaration.setName(ast.newSimpleName("main"));
		methodDeclaration.setReturnType(ast.newPrimitiveType(PrimitiveType.VOID));
		SingleVariableDeclaration variableDeclaration = ast.newSingleVariableDeclaration();
		variableDeclaration.setModifiers(Modifier.NONE);
		variableDeclaration.setType(ast.newArrayType(ast.newSimpleType(ast.newSimpleName("String"))));
		variableDeclaration.setName(ast.newSimpleName("args"));
		methodDeclaration.parameters().add(variableDeclaration);
		org.eclipse.jdt.core.dom.Block block = ast.newBlock();
		MethodInvocation methodInvocation = ast.newMethodInvocation();
		name = 
			ast.newQualifiedName(
				ast.newSimpleName("System"),
				ast.newSimpleName("out"));
		methodInvocation.setExpression(name);
		methodInvocation.setName(ast.newSimpleName("println"));
		InfixExpression infixExpression = ast.newInfixExpression();
		infixExpression.setOperator(InfixExpression.Operator.PLUS);
		StringLiteral literal = ast.newStringLiteral();
		literal.setLiteralValue("Hello");
		infixExpression.setLeftOperand(literal);
		literal = ast.newStringLiteral();
		literal.setLiteralValue(" world");
		infixExpression.setRightOperand(literal);
		methodInvocation.arguments().add(infixExpression);
		ExpressionStatement expressionStatement = ast.newExpressionStatement(methodInvocation);
		block.statements().add(expressionStatement);
		methodDeclaration.setBody(block);
		type.bodyDeclarations().add(methodDeclaration);
		unit.types().add(type);

检索额外的位置

DOM/AST 节点只包含一对位置(起始位置和节点的长度)。这并不总是够用。要检索中间位置,应使用 IScanner API。例如,我们具有想要对其了解 instanceof 运算符的位置的 InstanceofExpression。可以编写以下方法来实现此目的:
	private int[] getOperatorPosition(Expression expression, char[] source) {
		if (expression instanceof InstanceofExpression) {
			IScanner scanner = ToolFactory.createScanner(false, false, false, false);
			scanner.setSource(source);
			int start = expression.getStartPosition();
			int end = start + expression.getLength();
			scanner.resetTo(start, end);
			int token;
			try {
				while ((token = scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
					switch(token) {
						case ITerminalSymbols.TokenNameinstanceof:
							return new int[] {scanner.getCurrentTokenStartPosition(), scanner.getCurrentTokenEndPosition()};
					}
				}
			} catch (InvalidInputException e) {
			}
		}
      return null;
	}
IScanner 用来将输入源代码分成多个标记。每个标记都具有特定值,这些特定值是在 ITerminalSymbols 接口中定义的。迭代和检索正确的标记是非常简单的。如果想要在 SuperMethodInvocation 中查找 super 关键字的位置,则还建议您使用扫描程序。

一般源代码修改

某些源代码修改不是通过 Java 元素 API 来提供的。编辑源代码(例如,更改现有元素的源代码)的较常见方法是使用编译单元的原始源代码和 Java DOM 来完成的。

这些技术包括下列各项:

   // get the source for a compilation unit
String contents = myCompilationUnit.getBuffer().getContents();
   // Create an editable JDOM
   myJDOM = new DOMFactory();
   myDOMCompilationUnit = myJDOM.createCompilationUnit(contents, "MyClass");

   // Navigate and edit the compilation unit structure using
   // JDOM node protocol.
   ...
   // Once modififications have been made to all of the nodes
   // get the source back from the compilation unit DOM node.
   String newContents = myDOMCompilationUnit.getContents();

   // Set this code back into the compilation unit element
   myCompilationUnit.getBuffer().setContents(newContents);

   // Save the buffer to the file.
   myCompilationUnit.save(null, false);

此技术可能会导致问题标记与不正确的行号相关联,原因是未直接更新 Java 元素。

Java 元素模型同方法和字段差不多。编译器所使用的抽象语法树并不象 API 一样可用,因此,JDT 用来将源代码分析成程序化结构的技术当前也不能象 API 一样可用。

对 Java 元素的更改作出响应

如果事后插件需要知道对 Java 元素所作的更改,则可以向 JavaCore 注册 Java IElementChangedListener

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

还可以更具体,并使用 addElementChangedListener(IElementChangedListener, int) 来指定您感兴趣的事件的类型。

例如,如果您只想在构建器运行之前侦听事件:

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter(), ElementChangedEvent.PRE_AUTO_BUILD);

JavaCore 支持三种类型的事件:

Java 元素更改监听器在概念上类似于资源更改监听器 (在跟踪资源更改中进行了描述)。下列代码片段实现 Java 元素更改指示器,它将元素变化打印到系统控制台中。

public class MyJavaElementChangeReporter implements IElementChangedListener {      public void elementChanged(ElementChangedEvent event) {
         IJavaElementDelta delta= event.getDelta();
         if (delta != null) {
            System.out.println("delta received: ");
            System.out.print(delta);
         }
      }
   }

IJavaElementDelta 包括已更改的元素和描述所发生的更改类型的标志。在大多数时候,delta 树的根位于“Java 模型”级别。然后,客户机必须使用 getAffectedChildren 来浏览此 delta 以了解已经更改了哪些项目。

以下示例方法遍历 delta,并打印已添加、已除去和已更改的元素:

    void traverseAndPrint(IJavaElementDelta delta) {
         switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " was added");
                break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " was removed");
                break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " was changed");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("The change was in its children");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("The change was in its content");
                }
                /* Others flags can also be checked */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

有多种操作可以触发 Java 元素更改通知。以下是一些示例:

IResourceDelta 相似,可以使用 IWorkspaceRunnable 来对 Java 元素 delta 进行批处理。立即就会合并和报告在 IWorkspaceRunnable 中运行的若干“Java 模型”操作产生的 delta。  

JavaCore 提供了 run 方法来对 Java 元素更改进行批处理。

例如,以下代码段将触发 2 个 Java 元素更改事件:

    // Get package
    IPackageFragment pkg = ...;
    
    // Create 2 compilation units
    ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
    ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

而以下代码段将触发 1 个 Java 元素更改事件:

    // Get package
    IPackageFragment pkg = ...;
    
    // Create 2 compilation units
    JavaCore.run(
        new IWorkspaceRunnable() {
 	        public void run(IProgressMonitor monitor) throws CoreException {
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);
 	        }
        },
        null);

Copyright IBM Corporation and others 2000, 2003. All Rights Reserved.