在第 1 篇 - 如何编写一个面试时能拿的出手的开源项目? 博文中曾详细介绍过编写一个规范开源项目所要遵循的规范, 并且初步实现了博主自己的开源项目 Javac AST View 插件 https://github.com/mazhimazh/JavacASTViewer , 不过只搭建了项目开发的基本框架, 树状结构的数据模型也是硬编码的, 本篇博文将继续完善这个项目, 实现动态从 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 Windows = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
- if (Windows != null) {
- IWorkbenchPage page = Windows.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 插件开发学习笔记》一书
来源: https://www.cnblogs.com/extjs4/p/12370328.html