处理 Java 代码

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

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

使用 Java 元素来修改代码

生成编译单元

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

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

修改编译单元

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

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

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

工作副本

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

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

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

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

例如,下列代码段使用定制工作副本拥有者在编译单元上创建工作副本。该代码段将修改缓冲区、协调更改、将更改落实到磁盘,最后废弃工作副本。

    // Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Get working copy owner
    WorkingCopyOwner owner = ...;
    
    // Create working copy
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Modify buffer and reconcile
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Commit changes
    workingCopy.commitWorkingCopy(false, null);
    
    // Destroy working copy
    workingCopy.discardWorkingCopy();

工作副本还可以供若干使用工作副本拥有者的客户机共享。稍后可以使用 findWorkingCopy 方法来检索工作副本。因此,对于原始编译单元和工作副本拥有者,共享工作副本很重要。

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

    // Client 1 & 2: Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Get working copy owner
    WorkingCopyOwner owner = ...;
    
    // Client 1: Create shared working copy
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2: Retrieve shared working copy
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // This is the same working copy
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Discard shared working copy
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2: Attempt to retrieve shared working copy and find out it's null
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

使用 DOM/AST API 来修改代码

有三种方法来创建 CompilationUnit。第一种方法是使用 ASTParser。 第二种方法是使用 ICompilationUnit#reconcile(...)。第三种方法是从头开始对 AST(抽象语法树)使用工厂方法。

从现有的源代码创建 AST

必须使用 ASTParser.newParser(int) 来创建 ASTParser 的实例。

将使用下列其中一个方法将源代码提供给 ASTParser: 然后通过调用 createAST(IProgressMonitor) 来创建 AST。

结果是每个节点都有一个具有正确源位置的 AST。在使用 setResolveBindings(boolean) 创建树之前,必须请求绑定的解决方案。解决绑定是一个成本很高的操作,仅当需要时才执行。一旦修改了树,就会丢失所有位置和绑定。

通过协调工作副本来创建 AST

如果工作副本不一致(已修改),则可以通过调用方法 reconcile(int, boolean, org.eclipse.jdt.core.WorkingCopyOwner, org.eclipse.core.runtime.IProgressMonitor) 来创建 AST。要请求 AST 创建,使用 AST.JLS2 作为第一个参数来调用 reconcile(...) 方法。

仅当问题请求程序是活动或者当强制执行问题检测时,才会计算其绑定。解决绑定是一个成本很高的操作,仅当需要时才执行。一旦修改了树,就会丢失所有位置和绑定。

从头开始

可以通过对 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 来提供的。编辑源代码(例如,更改现有元素的源代码)的较常见方法是使用编译单元的初始源代码和 DOM/AST 的重写 API 来完成的。

要执行 DOM/AST 重写,存在两组 API:描述性重写和修改重写。

描述性 API 不修改 AST,而是使用 ASTRewrite API 来生成修改描述。AST 重写程序收集对节点修改的描述并将这些描述转换为可以适用于初始源代码的文本编辑。

   // creation of a Document
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // creation of DOM/AST from a ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // creation of ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // description of the change
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // computation of the text edits
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // computation of the new source code
   edits.apply(document);
   String newSource = document.get();

   // update of the compilation unit
   cu.getBuffer().setContents(newSource);

修改 API 允许直接修改 AST:

   // creation of a Document
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // creation of DOM/AST from a ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // start record of the modifications
   astRoot.recordModifications();

   // modify the AST
   TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0)
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   typeDeclaration.setName(newName);

   // computation of the text edits
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // computation of the new source code
   edits.apply(document);
   String newSource = document.get();

   // update of the compilation unit
   cu.getBuffer().setContents(newSource);

对 Java 元素的更改作出响应

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

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

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

例如,如果您只想在协调操作期间侦听事件:

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

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 元素更改进行批处理。

例如,以下代码段将触发两个 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);