在第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插件開發學習筆記》一書