引言
源代碼注釋是對代碼的解釋和說明。代碼注釋可以有效幫助程序 員規劃未完成的代碼任務,減少閱讀和理解陳舊代碼的時間成本,輔助定位可能 產生錯誤的代碼等,尤其在開發人員流動較大的情況下,代碼注釋的良莠直接關系到工作交接的執行效率甚至整個開發周期的時間和質量控制。清晰的代碼編程規范和詳細准確的代碼注釋已經成為評估軟件源代碼質量的重要參考標准之一。
Eclipse 作為目前最優秀的 Java 集成開發環境之一,雖然提供了代碼模板 用於定制代碼和注釋的格式,但它僅僅在第一次建立 Java 文件和自動插入代碼 片段時才會按模板定制內容插入預定義的注釋和代碼片段,這相對於漫長的代碼 維護過程是遠遠不夠的。比如:需要為已經存在的所有源代碼文件增加一份版權 聲明的注釋,Eclipse 提供的模版和格式化功能無法滿足類似的需求。本文提供 的工具,正是為彌補 Eclipse 模版功能的不足,使 Java 代碼及注釋可以在任 何時候更新到最新的模版,極大簡化維護代碼注釋與格式的工作量。
工具介紹
本文的解決方案是基於 Eclipse3.4 版本內置的 JDT(Java Development Tool)的基礎設施開發一個插件項目 Add Comment and Format。 此插件按照 Eclipse 工作空間首選項中代碼風格模板的設置為工作空間內的 Java 代碼添加、修改注釋並格式化 Java 源代碼。
讀者可下載此插件項 目,將其以已存在項目導入到 Eclipse 中,以“Run an Application”方式運行啟動項,創建新的 Eclipse 應用程序。可以看到 新的 Eclipse 應用程序中出現 Add Comment and Format工具欄按鈕(Action) (參見 圖 1)。點擊此按鈕即可觸發添加 Java 代碼注釋和格式化 Java 代碼 事件。
圖 1. Add Comment and Format 按鈕
下面通過一個實例來展示此工具的執行過程及效果。
首先,重新配置首選項 Code Template的注釋格式(參見 圖 2)。在本文示 例中操作如下:在 Code->New Java File模板中添加 plug in development ,刪除 ${filecomment};在 Comments-> Fields模板中將 ${field_type}添 加到字段名前,在 Methods模板中刪除 return。
圖 2. Eclipse Code Template 首選項配置頁面
然後,點擊工具欄中 Add Comment and Format按鈕(參見 圖 1),執行格 式化代碼及注釋功能並查看結果(參見 圖 3)。
圖 3. 修改完模板前後代碼比較
圖 3為工作空間中某一 Java 文件執行 Add Comment and Format 插件前後 的對比圖。圖左為執行之前,Eclipse 工作空間中的代碼與模版設定風格不一致 ,注釋添加參差不齊,而且已有的注釋內容也需要調整。右邊為執行之後, Eclipse 按照模板的設定格式為 Java 文件添加、修改、刪除注釋並格式化,具 有良好的代碼風格。
Add Comment and Format插件可以方便地更新工作空間內 Java 代碼風格, 保持代碼風格的一致與規范。它既沒有改變原有代碼的重要組成部分,也沒有產 生冗余的代碼注釋,大大減少人工修改的工作量和出錯率。
實現步驟:
實現 Add Comment and Format插件的功能主要包括如下步驟:
遍歷 Eclipse 工作空間獲取 Java 編譯單元
從 eclipse 工作空間的資源中獲得 Java 項目,並且遍歷此項目以獲得 Java 編譯單元,即工作區內的 Java 源文件(.java)。
得到 Java 編輯單元的工作副本緩存
工作副本是對 Java 源代碼進行修改時的緩存,可通過操作工作副本緩存來 修改代碼。
修改代碼
通過工作副本緩存修改代碼,按照模板重新生成 Java 代碼內容並替換原文 件,重新添加引用包列表,為 Java 代碼中方法、字段添加或修改注釋內容,並 及時與原文件同步。
格式化代碼
調用 Eclipse JDT 的格式化接口,通過操作工作副本緩存格式化代碼。
保存 Java 源文件
將對工作副本緩存的修改保存到對應的 Java 源文件中。
下面詳細討論每個步驟。
遍歷 Eclipse 工作空間獲取 Java 編譯單元
從體系結構看,JDT 分為模型和用戶界面兩部分。模型是 Java 語言規范中 Java 元素的抽象,比如:包、類、方法、字段等等。采用 JDT 提供的 Java 模 型操作代碼,比直接由 Java 源文件中取得和操作代碼的文本更加方便有效,而 且 Java 模型還可以感知其下的文件資源的變化。
Eclipse 工作空間的所有項目資源 (IProject) 可由 ResourcesPlugin 的靜 態方法獲取,得到工作空間的項目資源之後可以由 JavaCore 提供的靜態方法創 建 Java 模型的根元素 IJavaProject(參見 清單 1)。通過得到 IJavaProject 接口的實例就可以遍歷並得到 Java 的所有元素。
下面清單 1 至 4 給出遍歷 Java 元素,獲取 Java 編譯單元的代碼。
清單 1. 得到 Java 模型的 IJavaProject 元素
// 得到工作空間中的項目資源
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot ().getProjects();
for (IProject project : projects) {
// 根據工作空間資源創建 Java 模型的頂層元素(Java 項目元素)
IJavaProject javaProject = JavaCore.create (project);
……
}
}
清單 1給出得到 Java 模型的 IJavaProject 元素方法。由於 IJavaProject 元素是與資源相關的,即一個 IJavaProject 元素關聯到一個 Eclipse 項目資 源,所以在操作之前需要通過 exits() 方法判斷被關聯的資源是否存在,以避 免發生異常(參見 清單 2)。
清單 2. 判斷 Java 元素關聯的資源是否存在
IJavaProject javaProject = …… ;
// 判斷 Java 元素時候存在
if (javaProject.exists() && javaProject != null) {
……
}
包目錄包括源代碼文件夾目錄,Jar 庫以及一些附屬包。對於 Java 項目而 言,可以通過調用 IJavaProject 類的 getPackageFragmentRoots() 方法得到 的 IPackageFragmentRoot 集合。在此集合中,第一個元素就是源代碼文件夾目 錄,因此可直接取其‘ 0 ’元素(參見 清單 3)。
清單 3. 得到源代碼文件夾對應的 Java 元素
IJavaProject javaProject = …… ;
IPackageFragmentRoot root = javaProject.getPackageFragmentRoots()[0];
清單 4是遍歷源代碼文件夾中的 Java 元素(IPackageFragmentRoot),得 到包(IPackageFragment)中的 Java 編譯單元(ICompilationUnit)。
清單 4. 得到編譯單元
IPackageFragmentRoot root = ……;
for (IJavaElement pack : root.getChildren()) {
if (pack instanceof IPackageFragment) {
for (ICompilationUnit cu : ((IPackageFragment) pack).getCompilationUnits()) {
// 操作編譯單元(ICompilationUnit)
}
}
}
得到 Java 編輯單元的工作副本緩存
Java 代碼的可以通過操作工作副本進行修改。工作副本是代碼進行修改時的 分階段緩存區域,通過工作副本可以得到操作代碼的緩存。修改代碼可直接通過 操作工作副本的緩存來操作代碼,但必須及時與原文件保持同步,避免後續操作 與之沖突。修改完畢,提交將修改保存在磁盤上。為避免資源浪費,提交之後丟 棄工作副本。
其原理類似於常用的 Java 編譯器。在編譯器中,Java 代碼一旦打開,就會 產生一個工作副本,用戶保存代碼之前的所有操作均是對工作副本的操作。只有 進行關閉編輯器或保存代碼等提交代碼操作時,才會將文件保存到磁盤上。
本文中工具的實現是通過直接操作 Java 文件工作副本的緩存來修改 Java 文件的。首先,要根據編譯單元獲得工作副本,即將編譯單元切換到工作副本模 式(參見 清單 5)。
清單 5. 得到編譯單元的工作副本
parentCU.becomeWorkingCopy(new SubProgressMonitor(monitor, 1));
清單 5 中將編譯單元切換到工作副本模式,就是在內存中創建一塊存放 Java 代碼副本的地方,即工作副本緩存。
工作副本模式下,工作副本可以得到工作副本緩存,一個 IBuffer 的實例。 該實例類似於 StringBuffer 的 API,對其修改就可以達到修改與之關聯的 Java 元素的效果。在提交代碼之前,對緩存修改一直保存在工作副本中,直至 被顯示提交(參見 清單 6)。
清單 6. 得到工作副本緩存
// 得到工作副本緩存
IBuffer buffer = parentCU.getBuffer();
修改代碼
Eclipse 中 Java 代碼包含的注釋種類及順序是由 Code >> New Java File模板決定,注釋的具體內容由 Comments下相應的模板決定。為使工作空間 內的代碼具有一致的注釋風格,首先應按照代碼構建模板的形式重新構建代碼, 處理是否含有文件注釋、類注釋或其他的信息;接著,處理重新構建代碼時丟失 的重要信息,如引用包;然後,處理重新構建代碼時未處理的類體內部代碼注釋 ,如方法注釋和字段注釋;最後,將重新構建後的代碼格式化。這樣,Java 代 碼就具有了規范的注釋及良好的風格。
重新構建 Java 代碼
CodeGeneration(org.eclipse.jdt.ui)提供了獲取 Code Templates首選項 頁面的各類模板信息的重載靜態方法,並以字符串的形式返回,如文件注釋、類 注釋、方法注釋、字段注釋、新 Java 文件等。開發人員可擴展其方法得到不同 的模板信息。
在重新構建 Java 文件時,按照模板格式重新生成代碼,並替換原有代碼, 最後與原文件進行同步(參見 清單 7)。
清單 7. 重新構建 Java 代碼
// 得到 Java 代碼中的類
IType type = parentCU.getTypes()[0];
// 得到類的內容
String typeContent = type.getSource();
// 如果類含有 Javadoc,取類內容的子串,去除注釋內容
if (type.getJavadocRange() != null)
typeContent = typeContent.substring(type.getJavadocRange ().getOffset()
+ type.getJavadocRange().getLength() - type.getSourceRange ().getOffset());
// 調用 CodeGeneration 的獲得新 Java 文件的方法,重新構建代 碼
String content = CodeGeneration.getCompilationUnitContent (parentCU,
CodeGeneration.getTypeComment(parentCU, type.getElementName (),
lineDelimiter), typeContent, lineDelimiter);
// 用新得到的 Java 代碼替換原有代碼
buffer.replace(0, parentCU.getSourceRange().getLength(), content);
// 同步
JavaModelUtil.reconcile(parentCU);
清單 7重新構建 Java 代碼的步驟是調用 CodeGeneration 的 getCompilationUnitContent() 方法得到新 Java 代碼的內容,將其替換工作副 本緩存(IBuffer)中的 Java 代碼,再調用 reconcile() 方法將修改與工作副 本同步。
getCompilationUnitContent() 方法主要功能是讀取 Code >> New Java File模板,根據文件內容替換其中的表達式,返回一個具有格式(包含換 行符、空格)的字符串。該方法有 4 個參數,分別為編譯單元,類注釋內容, 類內容(不包含類注釋),項目的行分隔符。
類注釋內容由 CodeGeneration 的 getTypeComment() 方法得到,該方法讀 取 Comment >> Types模板並以字符串的類型返回。其中, getTypeComment() 方法的第二個參數是類標識符名稱,如文件 A.java 的類標 識符名稱就是 A。類標識符名稱由 IType 的 getElementName() 方法得到。
標志換行的行分隔符可由 StubUtility 的 getLineDelimiterUsed() 方法得 到(參見 清單 8),獲取類注釋、方法注釋、字段注釋模板內容的方法也同樣 需要此參數。
清單 8 得到項目行分隔符
String lineDelimiter = StubUtility.getLineDelimiterUsed (javaProject);
其中,類內容參數的處理最為復雜。使用 IType 的 getSource() 方法得到 的字符串不僅包含類聲明體的內容,而且包括類的 Javadoc 注釋。而傳遞給 getCompilationUnitContent() 方法的類內容參數中,不應包括類注釋。因此, 當類存在 javadoc 注釋時,需要將其去除。本文使用 String 的 substring() 方法,在 getSource() 得到的字符串中截取類內容。類聲明體內容的開始位置 即 javadoc 內容的結束位置,由於 Itype 的 getJavadocRange() 方法得到類 體最後的一個 javadoc 注釋區域范圍,這個范圍相對整個 Java 文件的結束位 置減去 IType 在 Java 文件中的絕對開始位置就得到此 JavaDoc 在 IType 類 getSource() 方法返回文本中的相對結束位置。
使用 IBuffer 替換原代碼的操作時,需確定處理內容的起始位置及長度。 IJavaElement 的 getSourceRange() 方法,可得到 Java 元素的區域范圍,起 始位置 (getOffset() 方法 ) 及長度 (getLength() 方法 )。由於 IJavaElement 是其他 Java 模型元素的父類,因此,Java 模型的元素均可使用 getSourceRange() 方法得到元素的區域范圍,並得到其元素內容的起始位置和 長度,在後續的實現中多次使用此方法確定處理元素內容的位置及長度。
在操作過程,對工作副本緩存的修改需要通知原資源,保證原文件與副本的 一致性,否則後續操作還是基於原文件進行,會覆蓋之前所做的操作。 JavaModelUtil 的 reconcile() 方法將觸發元素變化事件,保證文件的同步。
重新添加引用包
由於在重新構建 Java 代碼時,CodeGeneration 的 getCompilationUnitContent() 方法的實現中沒有涉及引用包的處理。因此,需 要重新為 Java 代碼添加引用包。
代碼 清單 9得到引用包的列表,以字符串列表的類型返回。
清單 9. 得到引用包列表
public List<String> getImport(ICompilationUnit parentCU,
String lineDelimiter) throws JavaModelException {
List<String> allImports = new ArrayList<String> ();
for (int i = 0; i < parentCU.getImports().length; i++) {
allImports.add(parentCU.getImports()[i].getElementName());
}
return allImports;
}
代碼 清單 10為 Java 代碼重新添加引用包列表。由於代碼重新構建且與原 文件同步後,就會完全丟失引用包的信息,需要在 清單 7執行之前獲得引用包 列表。代碼重新構建之後再使用 createImport() 方法為 Java 文件逐一重新添 加引用包,並及時與原文件同步。清單 10 中的省略號處的內容同清單 7。
清單 10. 重新添加引用包列表
List<String> allImports = getImport(parentCU, lineDelimiter);
……
// add import
for (String name : allImports) {
parentCU.createImport(name, null, monitor);
JavaModelUtil.reconcile(parentCU);
}
處理 Javadoc
由於代碼重新構建時,將類聲明體內容作為整體和其他內容重新組合,其內 部並沒有修改。因此,需要處理 Java 代碼內部的方法、字段的 javadoc 注釋 。獲取 Java 代碼中所有方法、字段,並識別此類型元素是否含有 Javadoc 注 釋,若含有,將此類型元素對應的模板注釋內容與原注釋替換;否則,為此元素 添加新的模板注釋內容。
清單 11通過 IType 得到對方法、字段操作的對象,並返回 IMenber 類型的 列表。
清單 11. 得到 Java 代碼中的 method、field
public List<IMember> getAllMember(IType type) throws JavaModelException {
List<IMember> list = new ArrayList<IMember> ();
// 得到所有方法,並添加到 list 中
for (IMethod method : type.getMethods()) {
list.add(method);
}
// 得到所有字段,並添加到 list 中
for (IField field : type.getFields()) {
list.add(field);
}
return list;
}
清單 12識別元素類型並針對類型得到不同的模板注釋;通過 IMember 的 getJavadocRange() 方法,判斷是否含有 javadoc,沒有則為此元素添加注釋; 否則,用重新讀取的模板注釋替換原有注釋內容。
清單 12. 處理 Javadoc
// 跟據元素類型不同得到不同的注釋模板內容
for (IMember member : getAllMember(type)) {
String comment = null;
switch (member.getElementType()) {
// 方法
case IJavaElement.METHOD:
comment = getMethodComment((IMethod) member, lineDelimiter);
break;
// 字段
case IJavaElement.FIELD:
comment = getFiledComment((IField) member, lineDelimiter);
break;
// 其他情況,返回類注釋
default:
comment = CodeGeneration.getTypeComment(parentCU,
type.getElementName(), lineDelimiter);
}
// 元素是否含有 Javadoc,沒有添加,有則替換
if (member.getJavadocRange() != null)
buffer.replace(member.getJavadocRange().getOffset(), member
.getJavadocRange().getLength(), comment);
else
buffer.replace(member.getSourceRange().getOffset(), 0, comment);
// 同步
JavaModelUtil.reconcile(copyCU);
}
清單 12利用 IJavaElement 的 getElementType() 方法得到類型屬性值,判 斷與哪種類型常量(IJavaElement.METHOD、IJavaElement.FIELD)匹配,識別 元素類型;若均不匹配,默認此元素類型是類。
getMethodComment()(清單 13)、getFiledComment()(清單 14)方法分別 得到方法、字段的模板內容。
清單 13. 得到方法注釋模板內容
public String getMethodComment(IMethod method, String lineDelimiter)
throws CoreException {
IType declaringType = method.getDeclaringType();
IMethod overridden = null;
if (!method.isConstructor()) {
ITypeHierarchy hierarchy = SuperTypeHierarchyCache
.getTypeHierarchy(declaringType);
MethodOverrideTester tester = new MethodOverrideTester(
declaringType, hierarchy);
overridden = tester.findOverriddenMethod(method, true);
}
return CodeGeneration.getMethodComment(method, overridden,
lineDelimiter);
}
清單 13是得到方法模板注釋的代碼片段。根據不同類型的方法(IMethod) 參數,讀取不同的模板,並均以字符串類型返回。若是構造方法,讀取 Comment >> Constructors中的模板;若是重載方法,讀取 Overriding methods中 的模板;若是其他方法,讀取 Methods中的模板。if 語句判斷方法參數是否為 構造函數,由於構造函數不能被重載,因此不需要在其父類中遞歸查找。
清單 14. 得到字段注釋模板內容
public String getFiledComment(IField field, String lineDelimiter)
throws IllegalArgumentException, CoreException {
String typeName = Signature.toString(field.getTypeSignature ());
String fieldName = field.getElementName();
return CodeGeneration.getFieldComment(field.getCompilationUnit (),
typeName, fieldName, lineDelimiter);
}
清單 14是得到 Comment >> Fields模板的 Javadoc 字符串。通過 IField 獲得字段名及字段類型,擴展 JDT 的方法。
格式化代碼
代碼修改後,為保證統一的排版格式,需進行代碼格式化,清單 15介紹 Java 代碼格式化排版的具體實現。
清單 15. 格式化排版
ICompilationUnit copyCU = ...;
IBuffer buffer = ...;
IType type = ...;
// 類體的范圍
ISourceRange sourceRange = type.getSourceRange();
// 獲得源代碼的類體內容
String originalContent = buffer.getText(sourceRange.getOffset (),
sourceRange.getLength());
// 代碼進行格式化
String formattedContent = CodeFormatterUtil.format(
CodeFormatter.K_CLASS_BODY_DECLARATIONS, originalContent,0,
lineDelimiter, copyCU.getJavaProject());
// 去除 tab 制表符和空格符
formattedContent = Strings.trimLeadingTabsAndSpaces (formattedContent);
// 替換原文件
buffer.replace(sourceRange.getOffset(), sourceRange.getLength (),
formattedContent);
代碼格式化仍是針對緩存的操作,調用 JDT 提供格式化接口 format() 方法 進行緩存格式化,最終反映到 Java 代碼上。format() 方法的第一個參數表示 格式化的類型,第三個參數是代表縮進級別的整數類型,小於等於 0 的值代表 沒有縮進,一級別代表一個 TAB 制表符的縮進度。
保存 Java 源文件
修改完畢,需要將代碼的變化提交才能保存到磁盤上。為了資源浪費,提交 之後,丟棄副本。清單 16是提交並丟棄副本的操作。
清單 16. 保存文件
IProgressMonitor monitor = ...;
ICompilationUnit copyCU = ...;
// 保存
copyCU.commitWorkingCopy(true, monitor);
if (copyCU != null)
// 丟棄副本
copyCU.discardWorkingCopy();
總之,針對單個編譯單元的修改的步驟是將工作單元轉化為工作副本,得到 工作副本緩存;修改緩存並定時與原文件同步;提交變化、丟棄副本來保存文件 。
結束語
本文介紹了一個為 Eclipse 工作空間中的 Java 代碼自動添加統一注釋並格 式化排版的工具及其具體實現。根據 Eclipse Code Template 的配置,通過擴 展 Eclipse Java Development Tool(JDT)API,來實現 Java 代碼與注釋的自 動格式化功能。通過本文,希望開發人員能夠更好地了解和應用 JDT 相關知識 ;通過擴展與配置 Eclipse 功能,讓 Eclipse 成為開發人員更加得心應手的可 配置開發平台。
原文地址:http://www.ibm.com/developerworks/cn/opensource/os-cn- ecl-cf/