1. 程式人生 > >第2篇-如何編寫一個面試時能拿的出手的開源專案?

第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外掛開發學習筆記》一書