第2篇-如何编写一个面试时能拿的出手的开源项目?
在第1篇-如何编写一个面试时能拿的出手的开源项目?博文中曾详细介绍过编写一个规范开源项目所要遵循的规范,并且初步实现了博主自己的开源项目Javac AST View插件,不过只搭建了项目开发的基本框架,树状结构的数据模型也是硬编码的,本篇博文将继续完善这个项目,实现动态从Eclipse编辑器中读取Java源代码,并在JavacASTViewer视图中展现Javac编译器的抽象语法树。实现过程中需要调用Javac的API接口获取抽象语法树,同时遍历这个抽象语法树,将其转换为Eclipse插件的树形视图所识别的数据模型。
下面我们基于上一篇博文所搭建的框架上继续进行开发。
首先需要对插件树形视图提供的数据模型进行修改,添加一些必要的属性,具体的源代码实现如下:
package astview; import java.util.ArrayList; import java.util.List; public class JavacASTNode { private String name; private String type; private String value; private List<JavacASTNode> children = null; private JavacASTNode parent = null; public JavacASTNode(String name, String type) { this.name = name; this.type = type; children = new ArrayList<JavacASTNode>(); } public JavacASTNode(String name, String type, String value) { this(name, type); this.value = value; } public JavacASTNode() { children = new ArrayList<JavacASTNode>(); } // 省略各属性的get与set方法 public String toString() { String display = name; if (type != null && type.length() > 0) { display = display + "={" + type.trim() + "}"; } else { display = display + "="; } if (value != null && value.length() > 0) { display = display + " " + value.trim(); } return display; } }
其中property表示属性名,如JCCompilationUnit树节点下有packageAnnotations、pid、defs等表示子树节点的属性;type为属性对应的定义类型;value为属性对应的值,这个值可选。这3个值在Eclipse树形中的显示格式由toString()方法定义。
现在我们需要修改内容提供者ViewContentProvider类中的getElements()方法,在这个方法中将Javac的抽象语法树转换为使用JavacASTNode表示的、符合Eclipse树形视图要求的数据模型。修改后的方法源代码如下:
public Object[] getElements(Object inputElement) { JavacASTNode root = null; if(inputElement instanceof JCCompilationUnit) { JavacASTVisitor visitor = new JavacASTVisitor(); root = visitor.traverse((JCCompilationUnit)inputElement); } return new JavacASTNode[] {root}; }
Javac用JCCompilationUnit来表示编译单元,可以简单认为一个Java源文件对应一个JCCompilationUnit实例。这里使用了JDK1.8的tools.jar包中提供的API,因为Javac的源代码包被打包到了这个压缩包中,所以需要将JDK1.8安装目录下的lib目录中的tools.jar引到项目中来。
JCCompilationUnit也是抽象语法树的根节点,遍历这个语法树并将每个语法树节点用JavacASTNode表示。使用访问者模式遍历抽象语法树。创建JavacASTVisitor类并继承TreeVisitor接口,如下:
package astview; import java.util.Set; import javax.lang.model.element.Modifier; import com.sun.source.tree.*; import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.*; import com.sun.tools.javac.util.List; public class JavacASTVisitor implements TreeVisitor<JavacASTNode, Void> { ... }
继承的接口TreeVisitor定义在com.sun.source.tree包下,是Javac为开发者提供的、遍历抽象语法树的访问者接口,接口的源代码如下:
public interface TreeVisitor<R,P> { R visitAnnotatedType(AnnotatedTypeTree node, P p); R visitAnnotation(AnnotationTree node, P p); R visitMethodInvocation(MethodInvocationTree node, P p); R visitAssert(AssertTree node, P p); R visitAssignment(AssignmentTree node, P p); R visitCompoundAssignment(CompoundAssignmentTree node, P p); R visitBinary(BinaryTree node, P p); R visitBlock(BlockTree node, P p); R visitBreak(BreakTree node, P p); R visitCase(CaseTree node, P p); R visitCatch(CatchTree node, P p); R visitClass(ClassTree node, P p); R visitConditionalExpression(ConditionalExpressionTree node, P p); R visitContinue(ContinueTree node, P p); R visitDoWhileLoop(DoWhileLoopTree node, P p); R visitErroneous(ErroneousTree node, P p); R visitExpressionStatement(ExpressionStatementTree node, P p); R visitEnhancedForLoop(EnhancedForLoopTree node, P p); R visitForLoop(ForLoopTree node, P p); R visitIdentifier(IdentifierTree node, P p); R visitIf(IfTree node, P p); R visitImport(ImportTree node, P p); R visitArrayAccess(ArrayAccessTree node, P p); R visitLabeledStatement(LabeledStatementTree node, P p); R visitLiteral(LiteralTree node, P p); R visitMethod(MethodTree node, P p); R visitModifiers(ModifiersTree node, P p); R visitNewArray(NewArrayTree node, P p); R visitNewClass(NewClassTree node, P p); R visitLambdaExpression(LambdaExpressionTree node, P p); R visitParenthesized(ParenthesizedTree node, P p); R visitReturn(ReturnTree node, P p); R visitMemberSelect(MemberSelectTree node, P p); R visitMemberReference(MemberReferenceTree node, P p); R visitEmptyStatement(EmptyStatementTree node, P p); R visitSwitch(SwitchTree node, P p); R visitSynchronized(SynchronizedTree node, P p); R visitThrow(ThrowTree node, P p); R visitCompilationUnit(CompilationUnitTree node, P p); R visitTry(TryTree node, P p); R visitParameterizedType(ParameterizedTypeTree node, P p); R visitUnionType(UnionTypeTree node, P p); R visitIntersectionType(IntersectionTypeTree node, P p); R visitArrayType(ArrayTypeTree node, P p); R visitTypeCast(TypeCastTree node, P p); R visitPrimitiveType(PrimitiveTypeTree node, P p); R visitTypeParameter(TypeParameterTree node, P p); R visitInstanceOf(InstanceOfTree node, P p); R visitUnary(UnaryTree node, P p); R visitVariable(VariableTree node, P p); R visitWhileLoop(WhileLoopTree node, P p); R visitWildcard(WildcardTree node, P p); R visitOther(Tree node, P p); }
定义的泛型类型中,R可以指定返回类型,而P可以额外为访问者方法指定参数。我们需要访问者方法返回转换后的JavacASTNode节点,所以R指定为了JavacASTNode类型,参数不需要额外指定,所以直接使用Void类型即可。
在TreeVisitor中定义了许多访问者方法,涉及到了抽象语法树的每个节点,这些节点在《深入解析Java编译器:源码剖析与实例详解》一书中详细做了介绍,有兴趣的可以参考。
接口中定义的访问者方法需要在JavacASTVisitor类中实现,例如对于visitCompilationUnit()方法、visitClass()方法、visitImport()方法及visitIdentifier()方法的具体实现如下:
@Override public JavacASTNode visitCompilationUnit(CompilationUnitTree node, Void p) { JCCompilationUnit t = (JCCompilationUnit) node; JavacASTNode currnode = new JavacASTNode(); currnode.setProperty("root"); currnode.setType(t.getClass().getSimpleName()); traverse(currnode,"packageAnnotations",t.packageAnnotations); traverse(currnode,"pid",t.pid); traverse(currnode,"defs",t.defs); return currnode; } @Override public JavacASTNode visitClass(ClassTree node, Void p) { JCClassDecl t = (JCClassDecl) node; JavacASTNode currnode = new JavacASTNode(); traverse(currnode,"extending",t.extending); traverse(currnode,"implementing",t.implementing); traverse(currnode,"defs",t.defs); return currnode; } public JavacASTNode visitImport(ImportTree node, Void curr) { JCImport t = (JCImport) node; JavacASTNode currnode = new JavacASTNode(); traverse(currnode,"qualid",t.qualid); return currnode; } @Override public JavacASTNode visitIdentifier(IdentifierTree node, Void p) { JCIdent t = (JCIdent) node; JavacASTNode currnode = new JavacASTNode(); JavacASTNode name = new JavacASTNode("name", t.name.getClass().getSimpleName(), t.name.toString()); currnode.addChild(name); name.setParent(currnode); return currnode; }
将JCCompilationUnit节点转换为JavacASTNode节点,并且调用traverse()方法继续处理子节点packageAnnotations、pid和defs。其它方法类似,这里不再过多介绍。更多关于访问者方法的实现可查看我的开源项目,地址为https://github.com/mazhimazh/JavacASTViewer
tranverse()方法的实现如下:
public JavacASTNode traverse(JCTree tree) { if (tree == null) return null; return tree.accept(this, null); } public void traverse(JavacASTNode parent, String property, JCTree currnode) { if (currnode == null) return; JavacASTNode sub = currnode.accept(this, null); sub.setProperty(property); if (sub.getType() == null) { sub.setType(currnode.getClass().getSimpleName()); } sub.setParent(parent); parent.addChild(sub); } public <T extends JCTree> void traverse(JavacASTNode parent, String property, List<T> trees) { if (trees == null || trees.size() == 0) return; JavacASTNode defs = new JavacASTNode(property, trees.getClass().getSimpleName()); defs.setParent(parent); parent.addChild(defs); for (int i = 0; i < trees.size(); i++) { JCTree tree = trees.get(i); JavacASTNode def_n = tree.accept(this, null); def_n.setProperty(i + ""); if (def_n.getType() == null) { def_n.setType(tree.getClass().getSimpleName()); } def_n.setParent(defs); defs.addChild(def_n); } }
为了方便对单个JCTree及列表List进行遍历,在JavacASTVisitor 类中定义了3个重载方法。在遍历列表时,列表的每一项的属性被指定为序号。
这样我们就将Javac的抽象语法树转换为Eclipse树形视图所需要的数据模型了。下面我们就来应用这个数据模型。
在JavacASTViewer插件启动时,读取Eclipse编辑器中的Java源代码,修改JavacASTViewer类的createPartControl()方法,具体实现如下:
public void createPartControl(Composite parent) { fViewer = new TreeViewer(parent, SWT.SINGLE); fViewer.setLabelProvider(new ViewLabelProvider()); fViewer.setContentProvider(new ViewContentProvider()); // fViewer.setInput(getSite()); try { IEditorPart part= EditorUtility.getActiveEditor(); if (part instanceof ITextEditor) { setInput((ITextEditor) part); } } catch (CoreException e) { // ignore } }
调用EditorUtility工具类的getActiveEditor()方法获取代表Eclipse当前激活的编辑器窗口,然后调用setInput()方法,这个方法的实现如下:
public void setInput(ITextEditor editor) throws CoreException { if (editor != null) { fEditor = editor; is = EditorUtility.getURI(editor); internalSetInput(is); } }
调用EditorUtility工具类的getURI()方法从当前激活的编辑器中获取Java源代码文件的路径,这个工具类的实现如下:
package astview; import java.net.URI; import org.eclipse.core.resources.IFile; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; public class EditorUtility { private EditorUtility() { super(); } public static IEditorPart getActiveEditor() { IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null) { IWorkbenchPage page = window.getActivePage(); if (page != null) { return page.getActiveEditor(); } } return null; } public static URI getURI(IEditorPart part) { IFile file = part.getEditorInput().getAdapter(IFile.class); return file.getLocationURI(); } }
继续看setInput()方法的实现,得到Java源文件的路径后,就需要调用Javac相关的API来解析这个Java源文件了,internalSetInput()方法的实现如下:
private JCCompilationUnit internalSetInput(URI is) throws CoreException { JCCompilationUnit root = null; try { root= createAST(is); resetView(root); if (root == null) { setContentDescription("AST could not be created."); return null; } } catch (RuntimeException e) { e.printStackTrace(); } return root; }
调用createAST()方法获取抽象语法树,调用resetView()方法为Eclipse的树形视图设置数据来源。
createAST()方法的实现如下:
JavacFileManager dfm = null; JavaCompiler comp = null; private JCCompilationUnit createAST(URI is) { if (comp == null) { Context context = new Context(); JavacFileManager.preRegister(context); JavaFileManager fileManager = context.get(JavaFileManager.class); comp = JavaCompiler.instance(context); dfm = (JavacFileManager) fileManager; } JavaFileObject jfo = dfm.getFileForInput(is.getPath()); JCCompilationUnit tree = comp.parse(jfo); return tree; }
调用Javac相关的API解析Java源代码,然后返回抽象语法树,在resetView()方法中将这个抽象语法树设置为树形视图的输入,如下:
private void resetView(JCCompilationUnit root) { fViewer.setInput(root); }
因为为fViewer设置的数据模型为JCCompilationUnit,所以当树形视图需要数据时,会调用JavacASTNode节点中的getElements()方法,接收到的参数inputElement的类型就是JCCompilationUnit的,这个方法我们在前面介绍过,这里不再介绍。
现在编写个实例来查看JavacASTViewer的显示效果,实例如下:
package test; import java.util.ArrayList; import java.util.List; public class Test { List<String> a = new ArrayList<String>(); String b; int c; public void test() { a.add("test"); b = "hello word!"; c = 1; } }
JavacASTViewer的显示效果如下:
后续文章将继续完善这个项目,包括为JavacASTViewer增加重新读取编辑器视图内容的“读入”按钮,双击抽象语法树的某个语法树节点后,Eclipse的编辑视图自动选中所对应的Java源代码,
增加测试用例及发布Eclipse插件安装地址等等。
参考:
(1)《深入解析Java编译器:源码剖析与实例详解》一书
(2)《Eclipse插件开发学习笔记》一书