第1篇-如何编写一个面试时能拿的出手的开源项目?博文中曾详细介绍过编写一个规范开源项目所要遵循的规范,并且初步实现了博主自己的开源项目Javac AST View插件,不过只搭建了项目开发的基本框架,树状结构的数据模型也是硬编码的,本篇博文将继续完善这个项目,实现动态从Eclipse编辑器中读取Java源代码,并在JavacASTViewer视图中展现Javac编译器的抽象语法树。实现过程中需要调用Javac的API接口获取抽象语法树,同时遍历这个抽象语法树,将其转换为Eclipse插件的树形视图所识别的数据模型。

下面我们基于上一篇博文所搭建的框架上继续进行开发。

首先需要对插件树形视图提供的数据模型进行修改,添加一些必要的属性,具体的源代码实现如下: 

  1. package astview;

  2. import java.util.ArrayList;
  3. import java.util.List;

  4. public class JavacASTNode {

  5.     private String name;
  6.     private String type;
  7.     private String value;

  8.     private List<JavacASTNode> children = null;
  9.     private JavacASTNode parent = null;

  10.     public JavacASTNode(String name, String type) {
  11.         this.name = name;
  12.         this.type = type;
  13.         children = new ArrayList<JavacASTNode>();
  14.     }

  15.     public JavacASTNode(String name, String type, String value) {
  16.         this(name, type);
  17.         this.value = value;
  18.     }

  19.     public JavacASTNode() {
  20.         children = new ArrayList<JavacASTNode>();
  21.     }

  22.     // 省略各属性的get与set方法

  23.     public String toString() {
  24.         String display = name;
  25.         if (type != null && type.length() > 0) {
  26.             display = display + "={" + type.trim() + "}";
  27.         } else {
  28.             display = display + "=";
  29.         }
  30.         if (value != null && value.length() > 0) {
  31.             display = display + " " + value.trim();
  32.         }
  33.         return display;
  34.     }

  35. }

其中property表示属性名,如JCCompilationUnit树节点下有packageAnnotations、pid、defs等表示子树节点的属性;type为属性对应的定义类型;value为属性对应的值,这个值可选。这3个值在Eclipse树形中的显示格式由toString()方法定义。 

现在我们需要修改内容提供者ViewContentProvider类中的getElements()方法,在这个方法中将Javac的抽象语法树转换为使用JavacASTNode表示的、符合Eclipse树形视图要求的数据模型。修改后的方法源代码如下:

  1. public Object[] getElements(Object inputElement) {
  2.   JavacASTNode root = null;
  3.   if(inputElement instanceof JCCompilationUnit) {
  4.      JavacASTVisitor visitor = new JavacASTVisitor();
  5.      root = visitor.traverse((JCCompilationUnit)inputElement);
  6.     }
  7.   return new JavacASTNode[] {root};
  8. }

Javac用JCCompilationUnit来表示编译单元,可以简单认为一个Java源文件对应一个JCCompilationUnit实例。这里使用了JDK1.8的tools.jar包中提供的API,因为Javac的源代码包被打包到了这个压缩包中,所以需要将JDK1.8安装目录下的lib目录中的tools.jar引到项目中来。

JCCompilationUnit也是抽象语法树的根节点,遍历这个语法树并将每个语法树节点用JavacASTNode表示。使用访问者模式遍历抽象语法树。创建JavacASTVisitor类并继承TreeVisitor接口,如下:

  1. package astview;

  2. import java.util.Set;
  3. import javax.lang.model.element.Modifier;
  4. import com.sun.source.tree.*;
  5. import com.sun.tools.javac.code.TypeTag;
  6. import com.sun.tools.javac.tree.JCTree;
  7. import com.sun.tools.javac.tree.JCTree.*;
  8. import com.sun.tools.javac.util.List;

  9. public class JavacASTVisitor implements TreeVisitor<JavacASTNode, Void> {    
  10.         ...
  11. }
继承的接口TreeVisitor定义在com.sun.source.tree包下,是Javac为开发者提供的、遍历抽象语法树的访问者接口,接口的源代码如下:
  1. public interface TreeVisitor<R,P> {
  2.     R visitAnnotatedType(AnnotatedTypeTree node, P p);
  3.     R visitAnnotation(AnnotationTree node, P p);
  4.     R visitMethodInvocation(MethodInvocationTree node, P p);
  5.     R visitAssert(AssertTree node, P p);
  6.     R visitAssignment(AssignmentTree node, P p);
  7.     R visitCompoundAssignment(CompoundAssignmentTree node, P p);
  8.     R visitBinary(BinaryTree node, P p);
  9.     R visitBlock(BlockTree node, P p);
  10.     R visitBreak(BreakTree node, P p);
  11.     R visitCase(CaseTree node, P p);
  12.     R visitCatch(CatchTree node, P p);
  13.     R visitClass(ClassTree node, P p);
  14.     R visitConditionalExpression(ConditionalExpressionTree node, P p);
  15.     R visitContinue(ContinueTree node, P p);
  16.     R visitDoWhileLoop(DoWhileLoopTree node, P p);
  17.     R visitErroneous(ErroneousTree node, P p);
  18.     R visitExpressionStatement(ExpressionStatementTree node, P p);
  19.     R visitEnhancedForLoop(EnhancedForLoopTree node, P p);
  20.     R visitForLoop(ForLoopTree node, P p);
  21.     R visitIdentifier(IdentifierTree node, P p);
  22.     R visitIf(IfTree node, P p);
  23.     R visitImport(ImportTree node, P p);
  24.     R visitArrayAccess(ArrayAccessTree node, P p);
  25.     R visitLabeledStatement(LabeledStatementTree node, P p);
  26.     R visitLiteral(LiteralTree node, P p);
  27.     R visitMethod(MethodTree node, P p);
  28.     R visitModifiers(ModifiersTree node, P p);
  29.     R visitNewArray(NewArrayTree node, P p);
  30.     R visitNewClass(NewClassTree node, P p);
  31.     R visitLambdaExpression(LambdaExpressionTree node, P p);
  32.     R visitParenthesized(ParenthesizedTree node, P p);
  33.     R visitReturn(ReturnTree node, P p);
  34.     R visitMemberSelect(MemberSelectTree node, P p);
  35.     R visitMemberReference(MemberReferenceTree node, P p);
  36.     R visitEmptyStatement(EmptyStatementTree node, P p);
  37.     R visitSwitch(SwitchTree node, P p);
  38.     R visitSynchronized(SynchronizedTree node, P p);
  39.     R visitThrow(ThrowTree node, P p);
  40.     R visitCompilationUnit(CompilationUnitTree node, P p);
  41.     R visitTry(TryTree node, P p);
  42.     R visitParameterizedType(ParameterizedTypeTree node, P p);
  43.     R visitUnionType(UnionTypeTree node, P p);
  44.     R visitIntersectionType(IntersectionTypeTree node, P p);
  45.     R visitArrayType(ArrayTypeTree node, P p);
  46.     R visitTypeCast(TypeCastTree node, P p);
  47.     R visitPrimitiveType(PrimitiveTypeTree node, P p);
  48.     R visitTypeParameter(TypeParameterTree node, P p);
  49.     R visitInstanceOf(InstanceOfTree node, P p);
  50.     R visitUnary(UnaryTree node, P p);
  51.     R visitVariable(VariableTree node, P p);
  52.     R visitWhileLoop(WhileLoopTree node, P p);
  53.     R visitWildcard(WildcardTree node, P p);
  54.     R visitOther(Tree node, P p);
  55. }

定义的泛型类型中,R可以指定返回类型,而P可以额外为访问者方法指定参数。我们需要访问者方法返回转换后的JavacASTNode节点,所以R指定为了JavacASTNode类型,参数不需要额外指定,所以直接使用Void类型即可。

在TreeVisitor中定义了许多访问者方法,涉及到了抽象语法树的每个节点,这些节点在《深入解析Java编译器:源码剖析与实例详解》一书中详细做了介绍,有兴趣的可以参考。

接口中定义的访问者方法需要在JavacASTVisitor类中实现,例如对于visitCompilationUnit()方法、visitClass()方法、visitImport()方法及visitIdentifier()方法的具体实现如下: 

  1. @Override
  2. public JavacASTNode visitCompilationUnit(CompilationUnitTree node, Void p) {
  3.     JCCompilationUnit t = (JCCompilationUnit) node;
  4.     JavacASTNode currnode = new JavacASTNode();
  5.     currnode.setProperty("root");
  6.     currnode.setType(t.getClass().getSimpleName());

  7.     traverse(currnode,"packageAnnotations",t.packageAnnotations);
  8.     traverse(currnode,"pid",t.pid);
  9.     traverse(currnode,"defs",t.defs);

  10.     return currnode;
  11. }

  12. @Override
  13. public JavacASTNode visitClass(ClassTree node, Void p) {
  14.     JCClassDecl t = (JCClassDecl) node;
  15.     JavacASTNode currnode = new JavacASTNode();

  16.     traverse(currnode,"extending",t.extending);
  17.     traverse(currnode,"implementing",t.implementing);
  18.     traverse(currnode,"defs",t.defs);
  19.     
  20.     return currnode;
  21. }

  22. public JavacASTNode visitImport(ImportTree node, Void curr) {
  23.     JCImport t = (JCImport) node;
  24.     JavacASTNode currnode = new JavacASTNode();
  25.     
  26.     traverse(currnode,"qualid",t.qualid);

  27.     return currnode;
  28. }

  29. @Override
  30. public JavacASTNode visitIdentifier(IdentifierTree node, Void p) {
  31.     JCIdent t = (JCIdent) node;
  32.     JavacASTNode currnode = new JavacASTNode();
  33.     JavacASTNode name = new JavacASTNode("name", t.name.getClass().getSimpleName(), t.name.toString());
  34.     currnode.addChild(name);
  35.     name.setParent(currnode);
  36.     return currnode;
  37. }

将JCCompilationUnit节点转换为JavacASTNode节点,并且调用traverse()方法继续处理子节点packageAnnotations、pid和defs。其它方法类似,这里不再过多介绍。更多关于访问者方法的实现可查看我的开源项目,地址为https://gitee.com/mazhimazh/JavacASTViewer

tranverse()方法的实现如下:

  1. public JavacASTNode traverse(JCTree tree) {
  2.     if (tree == null)
  3.         return null;
  4.     return tree.accept(this, null);
  5. }


  6. public void traverse(JavacASTNode parent, String property, JCTree currnode) {
  7.     if (currnode == null)
  8.         return;

  9.     JavacASTNode sub = currnode.accept(this, null);
  10.     sub.setProperty(property);
  11.     if (sub.getType() == null) {
  12.         sub.setType(currnode.getClass().getSimpleName());
  13.     }
  14.     sub.setParent(parent);
  15.     parent.addChild(sub);

  16. }

  17. public <T extends JCTree> void traverse(JavacASTNode parent, String property, List<T> trees) {
  18.     if (trees == null || trees.size() == 0)
  19.         return;

  20.     JavacASTNode defs = new JavacASTNode(property, trees.getClass().getSimpleName());
  21.     defs.setParent(parent);
  22.     parent.addChild(defs);

  23.     for (int i = 0; i < trees.size(); i++) {
  24.         JCTree tree = trees.get(i);
  25.         JavacASTNode def_n = tree.accept(this, null);
  26.         def_n.setProperty(i + "");
  27.         if (def_n.getType() == null) {
  28.             def_n.setType(tree.getClass().getSimpleName());
  29.         }
  30.         def_n.setParent(defs);

  31.         defs.addChild(def_n);
  32.     }
  33. }

为了方便对单个JCTree及列表List进行遍历,在JavacASTVisitor 类中定义了3个重载方法。在遍历列表时,列表的每一项的属性被指定为序号。

这样我们就将Javac的抽象语法树转换为Eclipse树形视图所需要的数据模型了。下面我们就来应用这个数据模型。

在JavacASTViewer插件启动时,读取Eclipse编辑器中的Java源代码,修改JavacASTViewer类的createPartControl()方法,具体实现如下: 

  1. public void createPartControl(Composite parent) {
  2.     
  3.     fViewer = new TreeViewer(parent, SWT.SINGLE);    
  4.     fViewer.setLabelProvider(new ViewLabelProvider());
  5.     fViewer.setContentProvider(new ViewContentProvider());
  6.         // fViewer.setInput(getSite());
  7.     
  8.     try {
  9.         IEditorPart part= EditorUtility.getActiveEditor();
  10.         if (part instanceof ITextEditor) {
  11.             setInput((ITextEditor) part);
  12.         }
  13.     } catch (CoreException e) {
  14.         // ignore
  15.     }
  16. }

调用EditorUtility工具类的getActiveEditor()方法获取代表Eclipse当前激活的编辑器窗口,然后调用setInput()方法,这个方法的实现如下:

  1. public void setInput(ITextEditor editor) throws CoreException {
  2.     if (editor != null) {
  3.         fEditor = editor;
  4.         is = EditorUtility.getURI(editor);
  5.         internalSetInput(is);
  6.     }
  7. }

调用EditorUtility工具类的getURI()方法从当前激活的编辑器中获取Java源代码文件的路径,这个工具类的实现如下:

  1. package astview;

  2. import java.net.URI;
  3. import org.eclipse.core.resources.IFile;
  4. import org.eclipse.ui.IEditorPart;
  5. import org.eclipse.ui.IWorkbenchPage;
  6. import org.eclipse.ui.IWorkbenchWindow;
  7. import org.eclipse.ui.PlatformUI;

  8. public class EditorUtility {
  9.     private EditorUtility() {
  10.         super();
  11.     }

  12.     public static IEditorPart getActiveEditor() {
  13.         IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
  14.         if (window != null) {
  15.             IWorkbenchPage page = window.getActivePage();
  16.             if (page != null) {
  17.                 return page.getActiveEditor();
  18.             }
  19.         }
  20.         return null;
  21.     }

  22.     public static URI getURI(IEditorPart part) {
  23.         IFile file = part.getEditorInput().getAdapter(IFile.class);
  24.         return file.getLocationURI();
  25.     }
  26. }
继续看setInput()方法的实现,得到Java源文件的路径后,就需要调用Javac相关的API来解析这个Java源文件了,internalSetInput()方法的实现如下:

  1. private JCCompilationUnit internalSetInput(URI is) throws CoreException {
  2.     JCCompilationUnit root = null;
  3.     try {
  4.         root= createAST(is);
  5.         resetView(root);
  6.         if (root == null) {
  7.             setContentDescription("AST could not be created.");
  8.             return null;
  9.         }
  10.     } catch (RuntimeException e) {
  11.         e.printStackTrace();
  12.     }
  13.     return root;
  14. }

调用createAST()方法获取抽象语法树,调用resetView()方法为Eclipse的树形视图设置数据来源。

createAST()方法的实现如下:

  1. JavacFileManager dfm = null;
  2. JavaCompiler comp = null;
  3. private JCCompilationUnit createAST(URI is) {
  4.     if (comp == null) {
  5.         Context context = new Context();
  6.         JavacFileManager.preRegister(context);
  7.         JavaFileManager fileManager = context.get(JavaFileManager.class);
  8.         comp = JavaCompiler.instance(context);
  9.         dfm = (JavacFileManager) fileManager;
  10.     }
  11.         
  12.     JavaFileObject jfo = dfm.getFileForInput(is.getPath());
  13.     JCCompilationUnit tree = comp.parse(jfo);
  14.     return tree;
  15. }

调用Javac相关的API解析Java源代码,然后返回抽象语法树,在resetView()方法中将这个抽象语法树设置为树形视图的输入,如下:

  1. private void resetView(JCCompilationUnit root) {
  2.      fViewer.setInput(root);
  3. }

因为为fViewer设置的数据模型为JCCompilationUnit,所以当树形视图需要数据时,会调用JavacASTNode节点中的getElements()方法,接收到的参数inputElement的类型就是JCCompilationUnit的,这个方法我们在前面介绍过,这里不再介绍。 

现在编写个实例来查看JavacASTViewer的显示效果,实例如下: 


  1. package test;

  2. import java.util.ArrayList;
  3. import java.util.List;

  4. public class Test {
  5.     List<String> a = new ArrayList<String>();
  6.     String b;
  7.     int c;

  8.     public void test() {
  9.         a.add("test");
  10.         b = "hello word!";
  11.         c = 1;
  12.     }
  13. }

JavacASTViewer的显示效果如下:
第2篇-如何编写一个面试时能拿的出手的开源项目?-LMLPHP

后续文章将继续完善这个项目,包括为JavacASTViewer增加重新读取编辑器视图内容的“读入”按钮,双击抽象语法树的某个语法树节点后,Eclipse的编辑视图自动选中所对应的Java源代码,

增加测试用例及发布Eclipse插件安装地址等等。  


 参考:

(1)《深入解析Java编译器:源码剖析与实例详解》一书

(2)《Eclipse插件开发学习笔记》一书

08-31 04:34