在Eclipse中創建新的重構功能
對重構的強大支持是軟件開發人員喜愛Eclipse的一個最為重要的原因。而Eclipse還有一個至少和重構不相上下的優點,那就是其近乎無懈可擊的可擴展性。這兩者的結合意味著我們可以根據自己的需要來創建展新的重構功能。
介紹
重構在現代軟件開發過程中扮演著重要的角色,它能夠減輕軟件開發人員的工作負擔,提高軟件開發的生產效率。為了闡明重構的重要性,我們在這里引用了developerWorks上David Carew提供的關于重構的教程中的一段話:
現在,一個開發者的工作大部分在于對現有的代碼進行修改,而不是起草寫新的代碼。簡單的修改可能包括對現有代碼進行添加。然而,多樣化的修改或擴展的改變會使軟件內部結構開始惡化。重構改變軟件的內部結構使得軟件更容易理解并且在不需要改變其顯著的行為的情況下使得修改的代價也更小。 在Java軟件開發過程中,通過使用Eclipse提供的重構工具,我們至少獲得了以下好處:
1. 最終產品更為健壯:我們對程序代碼的修改將不太可能出錯,出現遺漏修改的可能變少,即使出現問題也能夠通過Undo功能回退到重構前的狀態。
2. 提高了生產效率。通常一次重構能夠完成對程序代碼的多處改動。最為明顯的例子可能是Eclipse提供的Rename重構,它能夠在修改名稱的同時相應的更改所有的引用。 Eclipse為我們提供了多種實用的重構功能,在軟件開發過程中使用這些重構能夠給我們帶來極大的好處。然而,針對每個開發人員的特殊需要,總有一些迫切需要的功能是不能通過已有的重構來獲得的。這個時候,我們可以對Eclipse平臺進行一些擴展,創建適應我們自己需要的重構。如果這個重構恰好能夠符合大多數人的需要,我們也可以像其他Eclipse的contributor一樣,將我們的重構貢獻給Eclipse社區。
接下來,我們將通過一個例子來展示如何在Eclipse中創建新的重構功能。我們這里創建的重構將用于遷移JUnit的測試用例。我們知道,在當前版本的JUnit中,一個用于測試的函數必須以字符串"test"作為方法名稱的開始。而在即將來到的JUnit 4中,一個"@Test"的Annotation被用于標明方法是一個測試方法。我們將要創建的重構將完成這個遷移工作,即在所有的以"test"開始的方法之前加上"@Test"標記。@Test Annotation還可以包含一個timeout屬性用來規定方法的最大執行時間,我們在向導中提供了一個頁面供用戶選擇是否需要timeout屬性。
結果預覽
為了給讀者一個直觀的感受,我們下面首先介紹本文中例子的實際運行效果。在閱讀完本文之后,讀者朋友也能夠順利的完成類似的功能。
啟動例子程序提供的Refactor之后,我們獲得了一個由三個頁面組成的向導。在第一個頁面中,用戶可以選擇是否需要timeout參數,并且用戶能夠設置timeout參數的值。
當用戶輸入參數完畢之后,通過單擊Next按鈕我們將進入下一個頁面。向導將進行初始條件檢查和最終條件檢查,并將檢查的結果反饋給用戶。在圖 2中我們可以看到,初始條件和最終條件都正常,因此我們可以進入下一步。
接下來是預覽窗口(圖 3),向導用直觀的界面顯示了在應用向導之后,我們將會對源代碼造成怎樣的改動。用戶可以在這個頁面中判斷最終的修改是否符合自己的需要。另外,用戶也能夠選擇性的取消對某些文件的修改。
當用戶檢查預覽頁面確認沒有問題之后,用戶可以按下Finish按鈕從而完成重構。這個時候,源代碼會發生修改,最后的結果如下所示:
清單 1
總體結構和流程
在Eclipse中,一個重構操作主要由以下三個部分組成:
1. RefactoringWizard類:RefactoringWizard提供了向導式的用戶界面來引導用戶完成重構工作。不需要我們做任何工作,Eclipse已經通過RefactoringWizard為我們提供了預覽頁面、條件檢查頁面以及Undo/Redo等功能。我們需要繼承這個類從而為重構過程提供特定的用戶界面。
2. Refactoring類:Refactoring類完成具體的定位和修改代碼功能。為了建立新的Refactoring,我們需要繼承這個類并實現重構的邏輯部分。
3. AST和ASTParser:在Refactoring類中,我們需要對代碼進行定位和修改,這可以通過AST機制來完成。AST是abstract syntax tree的簡稱,它能夠將Java代碼解析成為一個樹形結構。在利用了AST樹之后,對源代碼的修改變成了對AST樹的遍歷、更改節點屬性,以及插入和刪除節點等。
一個典型的重構操作流程如下所示:
1. 用戶選擇要進行重構的對象,通過菜單項或按鈕啟動重構操作。
2. 創建具體的Refactoring類,彈出RefactoringWizard。
3. RefactoringWizard與用戶交互,引導用戶輸入必要的參數;RefactoringWizard調用Refactoring類的函數進行條件檢查。
4. Refactoring類創建AST,并利用其對源代碼進行定位和修改。這里進行的修改并不直接應用到源代碼上,而是被保存成Change對象,供Refactoring框架使用。
5. RefactoringWizard調用Refactoring類的函數,獲得重構內容的詳細描述信息(即第4步生成的Change對象),顯示在預覽界面上,待用戶確認。
6. 用戶確認后Refactoring框架將修改代碼,重構操作結束。
接下來,我們將詳細介紹新建重構類型的各個步驟。
創建插件工程
在大家對整個系統構架有了一個大概的了解之后,我們的介紹就從創建工程開始。大家都知道Eclipse提供了很好的擴展性,通過創建插件就能把我們要添加的重構功能無縫的插入到Eclipse平臺中。創建插件工程的方法在很多地方都有介紹,這里不再詳細講解。
通過菜單 File -> New-> Project,選擇Plug-in Project。點擊Next,出現對話框,輸入項目名稱manage.annotation,接受其他選項的默認值。點擊Next,出現插件屬性設置的對話框,繼續接受默認值。點擊Next,出現選擇插件模板對話框,該工程要在Refactor菜單中添加一個新的菜單項,所以這里我們采用"Hello,World"的插件模板。點擊Next,修改"Action類名稱"的值為AnnotationManageAction,點擊Finish按鈕。至此,一個最基本Eclipse工作臺的插件工程就被創建出來了。 插件工程創建后,缺省進入Plug-in開發透視圖,Plug-in Manifest編輯器自動打開,顯示這個插件工程的基本信息,如對其他插件的依賴,擴展點,構建(build)的配置信息等等。由于該工程需要用到其他插件的功能,必須為其添加到其他插件的依賴。在Plug-in Manifest編輯器點擊Dependencies頁面,在該頁面中的Required Plug-ins列表中通過Add按鈕添加如下的插件:
清單 2
或者也可以通過直接修改MANIFEST.MF文件完成。操作完成后察看MANIFEST.MF文件,注意Require-Bundle列表中是否出現了新添加的這幾項。MANIFEST.MF文件如下:
清單 3
在Plug-in Manifest編輯器中打開插件清單文件plugin.xml,可以看到,這個插件擴展了org.eclipse.ui.actionSets擴展點,這是一個基本的Eclipse工作臺的擴展點,通過擴展它,插件可以很簡單得對Eclipse的菜單、工具條進行擴展。這個plugin.xml是"Hello,World"插件模板的清單文件,我們把它改成適合這個工程的文件。清單如下:
清單 4
該清單文件表明,在Refactor菜單中添加了一個新菜單項"Annotation Manage",并在工具條上相應添加了一個按鈕。點擊菜單項或者按鈕的事件由類"manage.annotation.actions.AnnotationManageAction"處理。
最后需要修改的就是manage.annotation.actions.AnnotationManageAction類。它繼承了org.eclipse.ui.IWorkbenchWindowActionDelegate接口,該接口用于處理各種通過擴展點添加的操作。當菜單項或者按鈕被點擊時,這個類就被Eclipse工作臺裝載進來,處理轉發過來的請求以及接下來的操作。
AnnotationManageAction被創建后,一旦用戶的選擇部分有所改變,接口的selectionChanged函數就會被觸發,告知用戶所選擇的部分,可以在這個函數中根據用戶的選擇相應的修改操作的可用性或者其他顯示屬性。例如在本文的工程中,我們希望只有當用戶選擇了一個Java模型元素時才能使用這個操作,那么就需要在selectionChanged中添加如下的代碼:
清單 5
selectionChanged函數的參數selection記錄了用戶選擇的部分,我們首先判斷它的選擇部分的數目是否為一,然后判斷這個唯一的選擇部分是否為Java模型元素,這兩個條件任何一個不滿足都會導致action.setEnabled(false)的執行,這時會彈出如下的對話框說明操作不可用,同時菜單項和按鈕都會顯示成灰色,直到用戶選擇了合適的部分時,菜單項和按鈕才會實顯,就可以進行具體的操作了。
操作的執行是在AnnotationManageAction的run函數中實現的,例如在本文的工程中,就是彈出RefactoringWizard對話框,指導用戶完成重構的工作,這些我們將在下面的章節中講述。
擴展Refactoring類
通過前面系統構架的介紹,大家知道了Refactoring和RefactoringWizard是完成EClipse重構功能的基礎類。在創建好插件工程后,我們就通過擴展Refactoring來實現具體的功能。
Refactoring是所有支持代碼轉化的類的抽象父類,它在整個流程中與RefactoringWizard交互以完成重構的功能,起著非常重要的作用。這些類需要提供以下兩類方法:
用于條件檢查的方法,判斷重構操作大體而言能否執行,以及具體的轉化能否完成;
創建Change對象的方法,Change對象描述了所有將要執行的對當前代碼的修改操作。
Refactoring類的典型流程如下所示:
1. 具體的Refactoring類被創建。
2. 獲得用戶選擇的要進行重構的對象,初始化該Refactoring類。這個由具體的實現類給出相應的方法。
3. 在重構操作開始執行時,首先調用Refactoring的checkInitialConditions(IProgressMonitor) 基于用戶選擇的對象做一個的初始檢查,這個通常由界面自動執行。返回RefactoringStatus.FATAL表明初始檢查沒有通過,重構操作不能繼續。
4. 獲得進行重構的其他參數,比如,對重命名操作來說就是指新名字。這個通常是由界面根據用戶的輸入提供的。由具體的實現類給出相應的方法。
5. 獲得用戶輸入參數后,調用Refactoring的checkFinalConditions(IProgressMonitor)進行剩下的檢查,這個通常由界面自動執行,返回RefactoringStatus.FATAL表明最后的檢查沒有通過,重構操作不能繼續。
6. 調用Refactoring的createChange(IProgressMonitor)獲得Change對象,這個通常由界面自動執行,界面可以根據Change對象顯示預覽界面。
基于以上的介紹,為了實現本文工程中的重構操作,我們需要擴展Refactoring類,為它增加一個構造函數,并且具體實現checkInitialConditions、checkFinalConditions和createChange三個函數。
首先通過菜單File -> New->Class彈出創建類的對話框,輸入包名manage.annotation.refactor,類名AnnotationRefactoring,輸入父類org.eclipse.ltk.core.refactoring.Refactoring,選中"繼承抽象方法"復選框,點擊完成按鈕,一個擴展了Refactoring的最基本的類AnnotationRefactoring就被創建出來了。
首先為其增加構造函數,以用戶選擇的Java模型元素作為參數。Refactoring分析這個參數以得到所有相關的可寫Java文件,作為重構操作的對象,如果該模型元素包含在Java文件中,則找到包含它的文件節點;如果該模型元素包含Java文件,則找到它的所有子Java文件。構造函數代碼如下:
清單 6
接著完成checkInitialConditions函數,實現初始檢查的具體操作。作為示例,在本文工程中我們不進行任何具體的檢查操作,只簡單得給出初始檢查成功的信息,返回RefactoringStatus.
INFO以使重構操作繼續執行。checkInitialConditions函數代碼如下:
清單 7
接著完成checkFinalConditions函數,實現獲得用戶輸入參數后的后續檢查操作。在本文工程中,我們首先收集所有需要添加注釋的以test開頭的方法,判斷是否不存在這樣的方法,如果不存在給出出錯信息,返回RefactoringStatus.FATAL以結束重構操作;如果存在這樣的方法,則給出后續檢查成功的信息,返回RefactoringStatus.
INFO。checkFinalConditions函數代碼如下:
清單 8
最后,創建Change對象的createChange函數是整個重構操作中最核心的代碼,它的實現將在下面章節中介紹。
使用AST構造Change對象
當我們找到了修改的位置時,我們有兩個選擇:
1. 通過IScanner接口掃描代碼,然后通過IBuffer接口直接修改代碼
2. 通過遍歷和編輯AST樹進行結構化的修改
DeveloperWorks提供的文章《擴展Eclipse的Java開發工具》中,給出了使用IBuffer接口的例子。現在我們要講述使用AST來遍歷和修改Java源代碼的方法。
AST是abstract syntax tree的縮寫。它是Eclipse中的Java開發環境(JDT)為我們提供的極為強大的源代碼解析和編輯工具。
在使用AST樹提供的功能之前,我們首先要創建AST樹。由于AST樹的構建是一項費時的操作,JDT缺省情況下不將源代碼解析為AST樹。下面的代碼演示了獲得一個ICompilationUnit對應的AST樹的過程。在JDT提供的API中,ICompilationUnit接口用于表示一個可以被編譯的源代碼文件。在我們提供的例子程序中,我們通過下面的代碼將整個文件解析成為了一顆AST樹。
清單 9
AST樹中的每個節點都是ASTNode類型,通過Visit模式,我們可以訪問一個ASTNode包含的所有節點。下面的代碼演示了訪問一個AST節點并獲得其中所有的MethodDeclaration節點的方法。
清單 10
在收集到了所有的MethodDeclaration節點之后,我們就可以通過向AST樹中插入和刪除節點或者修改已有的節點的方法來修改AST樹了。下面的代碼演示了使用AST工具為方法添加@Test Annotation的功能。
清單 11
在Refactoring框架中,我們要求對AST樹的修改并不立刻反映到源代碼中。相反,我們需要一個能記錄整個修改過程的Change對象。Refactoring框架將利用這個Change對象來顯示Priveiw窗口、進行Undo和Redo等操作。大致上,我們記錄對一個AST樹的修改從而生成Change對象的過程如以下代碼所示。
清單 12
最后,由于Refactoring類的createChange方法僅返回一個Change對象,如果我們需要對多個源代碼文件進行修改,我們可以利用CompositeChange類將多個Change對象封裝成一個Change對象。這個過程可能類似如下代碼所執行的流程
清單 13
擴展RefactoringWizard 框架
Eclipse中的RefactoringWizard框架擴展了Eclipse的Wizard框架,關于Wizard框架的介紹可以在Eclipse的幫助系統中找到,這里我們僅從OO設計和架構的角度探討一下RefactoringWizard框架。
我們從Wizard相關的幾個類開始:
1. WizardPage類
WizardPage是一個包含了多個界面元素(比如文本框Text,按鈕Button)的一個界面組合部分。各個Page之間是獨立的,是可以動態加載的。WizardPage類的職責有:
·組合SWT界面元素,構造出一個界面頁。
·定義本身界面元素的操作行為。
在RefactoringWizard框架中預設了兩個通用的屬性頁:PreviewWizardPage和ErrorWizardPage。PreviewWizardPage類是用來預覽重構后的修改,對比代碼或其他資源的變化。ErrorWizardPage類是用來處理條件檢查及錯誤狀態通知的。我們只需擴展RefactoringWizard框架就可以自動獲取這兩項強大的功能。
2. Wizard類
一個Wizard就是一個裝載一系列WizardPage頁的容器,Wizard類的職責有:
·裝載一系列WizardPage,構造出一個復雜的界面。
·裝載領域類來處理具體業務邏輯。(在RefactoringWizard框架中這個類就是Refactoring類)
維護WizardPage頁之間以及頁與領域類之間的數據傳遞和狀態共享。(在這里要補充一點,其實在具體RefactoringWizard框架的實現中有專門的類來分擔這部分職責。)
我們的界面行為可以千變萬化(通過組合不同的WizardPage),而負責處理業務邏輯的領域類也可以獨立的變化,你可以隨意擴展Wizard的界面功能(-對擴展開放),而不用修改現有RefactoringWizard框架(-對修改封閉),這正是OO設計的最基本原則-OCP(Open-Close Principle)。
3. WizardDialog類
這個對話框類的主要職責是構造一個完整的GUI界面以及操作界面。它預設了一些按鈕(Back,Next,Finish,Cancel)等界面元素,它負責裝載Wizard類,操作時通過按鈕Back、Next來在多個WizardPage之間切換。
下面我們給出RefactoringWizard框架的架構圖:
從圖 5中我們可以看到,如果我們把每一個WizardPage頁看作一項業務,那么Refactoring正是處理業務邏輯的控制中心,它封裝了所有對業務邏輯的處理,當然它可以在將處理任務委任出去。但請注意,它并不負責實現業務流程,也就是說各業務(各個Page界面)之間的邏輯順序關系不由它維護。
RefactoringWizard框架充分考慮到了應用的可擴展性,它在SWT的MVC(模型-視圖-控制)元架構模式的基礎上,添加了一些新的架構元素。MVC模式促使業務邏輯與界面分離,界面與控制行為分離,而RefactoringWizard框架增強了界面本身分離的特性,它將一個完整的界面分拆成多個頁面,用戶可以動態組合這些頁面或添加新的頁面來擴展界面行為。這種特性-界面的動態組合,低耦合,高內聚,封裝良好的接口-讓我們領略到了OO設計的精髓。
下面我們通過以下幾個步驟來擴展RefactoringWizard框架:
·擴展RefactoringWizardPage
·擴展RefactoringWizard
·啟動RefactoringWizard
第一步,擴展RefactoringWizardPage:首先我們新建一個類AnnotationRefactoringWizardPage,它需要繼承UserInputWizardPage類(其父類是RefactoringWizardPage,而RefactoringWizardPage最終實現了IDialogPage接口)。接下來就是實現IDialogPage接口的createControl(…)方法,在這個方法里實現你的界面行為,比如我們例子中的TimeOut文本框,代碼清單如下:
清單 14
在這里我們要特別注意的一點是在定義完我們的界面元素后,需要將自定義的Composite納入框架的控制,就是這行代碼:"setControl(composite);"
在我們處理完輸入數據檢查后進入下一頁面之前,我們需要設置頁面完成的狀態,以及傳遞輸入數據到領域類Refactoring。我們用以下代碼設置好頁面完成的狀態后,下個頁面ErrorWizardPage就會處理顯示邏輯:
清單 15
傳遞輸入數據通過以下代碼來處理:
清單 16
其中getRefactoring()方法是繼承自RefactoringWizardPage的方法,由于我們的RefactoringWizard類裝載了RefactoringWizardPage和Refactoring類,這個方法是從RefactoringWizard類中獲得的,這里面用到了Observer設計模式。至此,我們完成RefactoringWizardPage的擴展。
第二步,擴展RefactoringWizard:首先我們新建一個類AnnotationRefactoringWizard,它需要繼承RefactoringWizard類,這個類中我們只需要加載定義好的AnnotationRefactoringWizardPage類和AnnotationRefactoring類,當然復雜的處理已經有RefactoringWizard框架處理好了。下面我們在構造函數中加載Refactoring類:
清單 17
然后我們加載我們的AnnotationRefactoringWizardPage類,只需重載父類RefactoringWizard的addUserInputPages()方法就可以:
清單 18
第三步,啟動RefactoringWizard。擴展好RefactoringWizard之后,就需要在用戶點擊菜單項或是按鈕時彈出這個對話框。RefactoringWizard最好使用RefactoringWizardOpenOperation類來打開(當然也可以用RefactoringWizardDialog)。RefactoringWizardOpenOperation首先進行重構的初始檢查,通過后才打開RefactoringWinzard對話框,否則就會打開錯誤對話框。前面完成創建插件工程時我們提到,彈出RefactoringWizard對話框的代碼應該放到響應菜單操作的類的run函數中。具體到本文工程中,就是把下面的代碼放到AnnotationManageAction的run函數中。這些代碼首先依次構造Refactoring和RefacoringWizard子類AnnotationRefactoring和AnnotationRefactoringWizard,并將AnnotationRefactoring的引用傳遞給AnnotationRefactoringWizard,然后用RefactoringWizardOpenOperation打開AnnotationRefactoringWizard,彈出向導對話框。
清單 19
小結
在Eclipse中有效的利用重構能夠大大的減輕軟件開發人員的工作負擔,提高軟件的健壯性。然而,目前重構仍然處在一個工具缺乏的時代。以Eclipse為例,只有JDT提供的重構工具最為完善,而針對其他語言例如C++、Python等的開發環境,都缺乏對應的重構功能。 通過本文提供的方法,我們能夠有效的利用Eclipse中的重構框架創建新的重構,從而進一步提高已有開發環境的效率。
介紹
重構在現代軟件開發過程中扮演著重要的角色,它能夠減輕軟件開發人員的工作負擔,提高軟件開發的生產效率。為了闡明重構的重要性,我們在這里引用了developerWorks上David Carew提供的關于重構的教程中的一段話:
現在,一個開發者的工作大部分在于對現有的代碼進行修改,而不是起草寫新的代碼。簡單的修改可能包括對現有代碼進行添加。然而,多樣化的修改或擴展的改變會使軟件內部結構開始惡化。重構改變軟件的內部結構使得軟件更容易理解并且在不需要改變其顯著的行為的情況下使得修改的代價也更小。 在Java軟件開發過程中,通過使用Eclipse提供的重構工具,我們至少獲得了以下好處:
1. 最終產品更為健壯:我們對程序代碼的修改將不太可能出錯,出現遺漏修改的可能變少,即使出現問題也能夠通過Undo功能回退到重構前的狀態。
2. 提高了生產效率。通常一次重構能夠完成對程序代碼的多處改動。最為明顯的例子可能是Eclipse提供的Rename重構,它能夠在修改名稱的同時相應的更改所有的引用。 Eclipse為我們提供了多種實用的重構功能,在軟件開發過程中使用這些重構能夠給我們帶來極大的好處。然而,針對每個開發人員的特殊需要,總有一些迫切需要的功能是不能通過已有的重構來獲得的。這個時候,我們可以對Eclipse平臺進行一些擴展,創建適應我們自己需要的重構。如果這個重構恰好能夠符合大多數人的需要,我們也可以像其他Eclipse的contributor一樣,將我們的重構貢獻給Eclipse社區。
接下來,我們將通過一個例子來展示如何在Eclipse中創建新的重構功能。我們這里創建的重構將用于遷移JUnit的測試用例。我們知道,在當前版本的JUnit中,一個用于測試的函數必須以字符串"test"作為方法名稱的開始。而在即將來到的JUnit 4中,一個"@Test"的Annotation被用于標明方法是一個測試方法。我們將要創建的重構將完成這個遷移工作,即在所有的以"test"開始的方法之前加上"@Test"標記。@Test Annotation還可以包含一個timeout屬性用來規定方法的最大執行時間,我們在向導中提供了一個頁面供用戶選擇是否需要timeout屬性。
結果預覽
為了給讀者一個直觀的感受,我們下面首先介紹本文中例子的實際運行效果。在閱讀完本文之后,讀者朋友也能夠順利的完成類似的功能。
啟動例子程序提供的Refactor之后,我們獲得了一個由三個頁面組成的向導。在第一個頁面中,用戶可以選擇是否需要timeout參數,并且用戶能夠設置timeout參數的值。
![]() 圖 1 輸入參數 |
當用戶輸入參數完畢之后,通過單擊Next按鈕我們將進入下一個頁面。向導將進行初始條件檢查和最終條件檢查,并將檢查的結果反饋給用戶。在圖 2中我們可以看到,初始條件和最終條件都正常,因此我們可以進入下一步。
![]() 圖 2 顯示條件檢查 |
接下來是預覽窗口(圖 3),向導用直觀的界面顯示了在應用向導之后,我們將會對源代碼造成怎樣的改動。用戶可以在這個頁面中判斷最終的修改是否符合自己的需要。另外,用戶也能夠選擇性的取消對某些文件的修改。

當用戶檢查預覽頁面確認沒有問題之后,用戶可以按下Finish按鈕從而完成重構。這個時候,源代碼會發生修改,最后的結果如下所示:
清單 1
| package main; public class TestSomething { @Test(timeout=500) public void testSomething(){} } |
總體結構和流程
在Eclipse中,一個重構操作主要由以下三個部分組成:
1. RefactoringWizard類:RefactoringWizard提供了向導式的用戶界面來引導用戶完成重構工作。不需要我們做任何工作,Eclipse已經通過RefactoringWizard為我們提供了預覽頁面、條件檢查頁面以及Undo/Redo等功能。我們需要繼承這個類從而為重構過程提供特定的用戶界面。
2. Refactoring類:Refactoring類完成具體的定位和修改代碼功能。為了建立新的Refactoring,我們需要繼承這個類并實現重構的邏輯部分。
3. AST和ASTParser:在Refactoring類中,我們需要對代碼進行定位和修改,這可以通過AST機制來完成。AST是abstract syntax tree的簡稱,它能夠將Java代碼解析成為一個樹形結構。在利用了AST樹之后,對源代碼的修改變成了對AST樹的遍歷、更改節點屬性,以及插入和刪除節點等。
一個典型的重構操作流程如下所示:
1. 用戶選擇要進行重構的對象,通過菜單項或按鈕啟動重構操作。
2. 創建具體的Refactoring類,彈出RefactoringWizard。
3. RefactoringWizard與用戶交互,引導用戶輸入必要的參數;RefactoringWizard調用Refactoring類的函數進行條件檢查。
4. Refactoring類創建AST,并利用其對源代碼進行定位和修改。這里進行的修改并不直接應用到源代碼上,而是被保存成Change對象,供Refactoring框架使用。
5. RefactoringWizard調用Refactoring類的函數,獲得重構內容的詳細描述信息(即第4步生成的Change對象),顯示在預覽界面上,待用戶確認。
6. 用戶確認后Refactoring框架將修改代碼,重構操作結束。
接下來,我們將詳細介紹新建重構類型的各個步驟。
創建插件工程
在大家對整個系統構架有了一個大概的了解之后,我們的介紹就從創建工程開始。大家都知道Eclipse提供了很好的擴展性,通過創建插件就能把我們要添加的重構功能無縫的插入到Eclipse平臺中。創建插件工程的方法在很多地方都有介紹,這里不再詳細講解。
通過菜單 File -> New-> Project,選擇Plug-in Project。點擊Next,出現對話框,輸入項目名稱manage.annotation,接受其他選項的默認值。點擊Next,出現插件屬性設置的對話框,繼續接受默認值。點擊Next,出現選擇插件模板對話框,該工程要在Refactor菜單中添加一個新的菜單項,所以這里我們采用"Hello,World"的插件模板。點擊Next,修改"Action類名稱"的值為AnnotationManageAction,點擊Finish按鈕。至此,一個最基本Eclipse工作臺的插件工程就被創建出來了。 插件工程創建后,缺省進入Plug-in開發透視圖,Plug-in Manifest編輯器自動打開,顯示這個插件工程的基本信息,如對其他插件的依賴,擴展點,構建(build)的配置信息等等。由于該工程需要用到其他插件的功能,必須為其添加到其他插件的依賴。在Plug-in Manifest編輯器點擊Dependencies頁面,在該頁面中的Required Plug-ins列表中通過Add按鈕添加如下的插件:
清單 2
| org.eclipse.jface.text org.eclipse.ltk.core.refactoring org.eclipse.ltk.ui.refactoring org.eclipse.jdt org.eclipse.jdt.core |
或者也可以通過直接修改MANIFEST.MF文件完成。操作完成后察看MANIFEST.MF文件,注意Require-Bundle列表中是否出現了新添加的這幾項。MANIFEST.MF文件如下:
清單 3
| Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Annotation Plug-in Bundle-SymbolicName: manage.annotation; singleton:=true Bundle-Version: 1.0.0 Bundle-Activator: manage.annotation.AnnotationPlugin Bundle-Localization: plugin Require-Bundle: org.eclipse.ui, org.eclipse.core.runtime, org.eclipse.jface.text, org.eclipse.ltk.core.refactoring, org.eclipse.ltk.ui.refactoring, org.eclipse.jdt, org.eclipse.jdt.core Eclipse-AutoStart: true |
在Plug-in Manifest編輯器中打開插件清單文件plugin.xml,可以看到,這個插件擴展了org.eclipse.ui.actionSets擴展點,這是一個基本的Eclipse工作臺的擴展點,通過擴展它,插件可以很簡單得對Eclipse的菜單、工具條進行擴展。這個plugin.xml是"Hello,World"插件模板的清單文件,我們把它改成適合這個工程的文件。清單如下:
清單 4
| <?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.0"?> <plugin> <extension point="org.eclipse.ui.actionSets"> <actionSet label="Annotation Action Set" visible="true" id="manage.annotation.actionSet"> <menu label="%Refactoring.menu.label" path="source" id="org.eclipse.jdt.ui.refactoring.menu"> <separator name="reorgGroup"/> </menu> <action class="manage.annotation.actions.AnnotationManageAction" icon="icons/sample.gif" id="manage.annotation.actions.AnnotationManageAction" label="%Annotation.manage" menubarPath="org.eclipse.jdt.ui.refactoring.menu/reorgGroup" toolbarPath="reorgGroup" tooltip="Manage Annotation in Java Project"/> </actionSet> </extension> </plugin> |
該清單文件表明,在Refactor菜單中添加了一個新菜單項"Annotation Manage",并在工具條上相應添加了一個按鈕。點擊菜單項或者按鈕的事件由類"manage.annotation.actions.AnnotationManageAction"處理。
最后需要修改的就是manage.annotation.actions.AnnotationManageAction類。它繼承了org.eclipse.ui.IWorkbenchWindowActionDelegate接口,該接口用于處理各種通過擴展點添加的操作。當菜單項或者按鈕被點擊時,這個類就被Eclipse工作臺裝載進來,處理轉發過來的請求以及接下來的操作。
AnnotationManageAction被創建后,一旦用戶的選擇部分有所改變,接口的selectionChanged函數就會被觸發,告知用戶所選擇的部分,可以在這個函數中根據用戶的選擇相應的修改操作的可用性或者其他顯示屬性。例如在本文的工程中,我們希望只有當用戶選擇了一個Java模型元素時才能使用這個操作,那么就需要在selectionChanged中添加如下的代碼:
清單 5
| public void selectionChanged(IAction action, ISelection selection) { if (selection.isEmpty()) select = null; else if (selection instanceof IStructuredSelection) { IStructuredSelection strut = ((IStructuredSelection) selection); if (strut.size() != 1) select = null; if (strut.getFirstElement() instanceof IJavaElement) select = (IJavaElement) strut.getFirstElement(); } else select = null; action.setEnabled(select != null); } |
selectionChanged函數的參數selection記錄了用戶選擇的部分,我們首先判斷它的選擇部分的數目是否為一,然后判斷這個唯一的選擇部分是否為Java模型元素,這兩個條件任何一個不滿足都會導致action.setEnabled(false)的執行,這時會彈出如下的對話框說明操作不可用,同時菜單項和按鈕都會顯示成灰色,直到用戶選擇了合適的部分時,菜單項和按鈕才會實顯,就可以進行具體的操作了。
![]() 圖 4 表明Action目前不能執行的對話框 |
操作的執行是在AnnotationManageAction的run函數中實現的,例如在本文的工程中,就是彈出RefactoringWizard對話框,指導用戶完成重構的工作,這些我們將在下面的章節中講述。
擴展Refactoring類
通過前面系統構架的介紹,大家知道了Refactoring和RefactoringWizard是完成EClipse重構功能的基礎類。在創建好插件工程后,我們就通過擴展Refactoring來實現具體的功能。
Refactoring是所有支持代碼轉化的類的抽象父類,它在整個流程中與RefactoringWizard交互以完成重構的功能,起著非常重要的作用。這些類需要提供以下兩類方法:
用于條件檢查的方法,判斷重構操作大體而言能否執行,以及具體的轉化能否完成;
創建Change對象的方法,Change對象描述了所有將要執行的對當前代碼的修改操作。
Refactoring類的典型流程如下所示:
1. 具體的Refactoring類被創建。
2. 獲得用戶選擇的要進行重構的對象,初始化該Refactoring類。這個由具體的實現類給出相應的方法。
3. 在重構操作開始執行時,首先調用Refactoring的checkInitialConditions(IProgressMonitor) 基于用戶選擇的對象做一個的初始檢查,這個通常由界面自動執行。返回RefactoringStatus.FATAL表明初始檢查沒有通過,重構操作不能繼續。
4. 獲得進行重構的其他參數,比如,對重命名操作來說就是指新名字。這個通常是由界面根據用戶的輸入提供的。由具體的實現類給出相應的方法。
5. 獲得用戶輸入參數后,調用Refactoring的checkFinalConditions(IProgressMonitor)進行剩下的檢查,這個通常由界面自動執行,返回RefactoringStatus.FATAL表明最后的檢查沒有通過,重構操作不能繼續。
6. 調用Refactoring的createChange(IProgressMonitor)獲得Change對象,這個通常由界面自動執行,界面可以根據Change對象顯示預覽界面。
基于以上的介紹,為了實現本文工程中的重構操作,我們需要擴展Refactoring類,為它增加一個構造函數,并且具體實現checkInitialConditions、checkFinalConditions和createChange三個函數。
首先通過菜單File -> New->Class彈出創建類的對話框,輸入包名manage.annotation.refactor,類名AnnotationRefactoring,輸入父類org.eclipse.ltk.core.refactoring.Refactoring,選中"繼承抽象方法"復選框,點擊完成按鈕,一個擴展了Refactoring的最基本的類AnnotationRefactoring就被創建出來了。
首先為其增加構造函數,以用戶選擇的Java模型元素作為參數。Refactoring分析這個參數以得到所有相關的可寫Java文件,作為重構操作的對象,如果該模型元素包含在Java文件中,則找到包含它的文件節點;如果該模型元素包含Java文件,則找到它的所有子Java文件。構造函數代碼如下:
清單 6
| public AnnotationRefactoring(IJavaElement element) { while (element.getElementType() > IJavaElement.COMPILATION_UNIT) { element = element.getParent(); if (element == null) return; } if (element.getElementType() == IJavaElement.COMPILATION_UNIT) { if (!element.isReadOnly()) compilationUnits.add(element); } if (element.getElementType() < IJavaElement.COMPILATION_UNIT) findWritableCompilationUnits(element); } |
接著完成checkInitialConditions函數,實現初始檢查的具體操作。作為示例,在本文工程中我們不進行任何具體的檢查操作,只簡單得給出初始檢查成功的信息,返回RefactoringStatus.
INFO以使重構操作繼續執行。checkInitialConditions函數代碼如下:
清單 7
| public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { return RefactoringStatus.createInfoStatus("Initial Condition is OK!"); } |
接著完成checkFinalConditions函數,實現獲得用戶輸入參數后的后續檢查操作。在本文工程中,我們首先收集所有需要添加注釋的以test開頭的方法,判斷是否不存在這樣的方法,如果不存在給出出錯信息,返回RefactoringStatus.FATAL以結束重構操作;如果存在這樣的方法,則給出后續檢查成功的信息,返回RefactoringStatus.
INFO。checkFinalConditions函數代碼如下:
清單 8
| public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { collectChanges(); if (fChangeManager.size() == 0) return RefactoringStatus.createFatalErrorStatus("No testing methods found!"); else return RefactoringStatus.createInfoStatus("Final condition is OK!"); } |
最后,創建Change對象的createChange函數是整個重構操作中最核心的代碼,它的實現將在下面章節中介紹。
使用AST構造Change對象
當我們找到了修改的位置時,我們有兩個選擇:
1. 通過IScanner接口掃描代碼,然后通過IBuffer接口直接修改代碼
2. 通過遍歷和編輯AST樹進行結構化的修改
DeveloperWorks提供的文章《擴展Eclipse的Java開發工具》中,給出了使用IBuffer接口的例子。現在我們要講述使用AST來遍歷和修改Java源代碼的方法。
AST是abstract syntax tree的縮寫。它是Eclipse中的Java開發環境(JDT)為我們提供的極為強大的源代碼解析和編輯工具。
在使用AST樹提供的功能之前,我們首先要創建AST樹。由于AST樹的構建是一項費時的操作,JDT缺省情況下不將源代碼解析為AST樹。下面的代碼演示了獲得一個ICompilationUnit對應的AST樹的過程。在JDT提供的API中,ICompilationUnit接口用于表示一個可以被編譯的源代碼文件。在我們提供的例子程序中,我們通過下面的代碼將整個文件解析成為了一顆AST樹。
清單 9
| ASTParser parser = ASTParser.newParser(AST.JLS3); parser.setSource(cu); ASTNode root = parser.createAST(null); |
AST樹中的每個節點都是ASTNode類型,通過Visit模式,我們可以訪問一個ASTNode包含的所有節點。下面的代碼演示了訪問一個AST節點并獲得其中所有的MethodDeclaration節點的方法。
清單 10
| private void getMethods(ASTNode cuu, final List methods) { cuu.accept(new ASTVisitor() { public boolean visit(MethodDeclaration node) { methods.add(node); return false; } }); } |
在收集到了所有的MethodDeclaration節點之后,我們就可以通過向AST樹中插入和刪除節點或者修改已有的節點的方法來修改AST樹了。下面的代碼演示了使用AST工具為方法添加@Test Annotation的功能。
清單 11
| private boolean collectChanges(CompilationUnit root,MethodDeclaration method) { if (method.getName().getFullyQualifiedName().startsWith("test")) { AST ast = method.getAST(); if (needTimeout) { NormalAnnotation na = ast.newNormalAnnotation(); na.setTypeName(ast.newSimpleName("Test")); MemberValuePair pair = ast.newMemberValuePair(); pair.setName(ast.newSimpleName("timeout")); pair.setValue(ast.newNumberLiteral("500")); na.values().add(pair); method.modifiers().add(0, na); } else { MarkerAnnotation na = ast.newMarkerAnnotation(); na.setTypeName(ast.newSimpleName("Test")); method.modifiers().add(0, na); } return true; } return false; } |
在Refactoring框架中,我們要求對AST樹的修改并不立刻反映到源代碼中。相反,我們需要一個能記錄整個修改過程的Change對象。Refactoring框架將利用這個Change對象來顯示Priveiw窗口、進行Undo和Redo等操作。大致上,我們記錄對一個AST樹的修改從而生成Change對象的過程如以下代碼所示。
清單 12
| root.recordModifications(); //在這里修改AST樹… TextEdit edits = root.rewrite(document, cu.getJavaProject() .getOptions(true)); TextFileChange change = new TextFileChange("", (IFile) cu .getResource()); change.setEdit(edits); |
最后,由于Refactoring類的createChange方法僅返回一個Change對象,如果我們需要對多個源代碼文件進行修改,我們可以利用CompositeChange類將多個Change對象封裝成一個Change對象。這個過程可能類似如下代碼所執行的流程
清單 13
| public Change createChange(IProgressMonitor pm) throws CoreException,OperationCanceledException { Change[] changes = new Change[fChangeManager.size()]; System.arraycopy(fChangeManager.toArray(), 0, changes, 0,fChangeManager.size()); CompositeChange change = new CompositeChange("Add @Override Annotation", changes); return change; } |
擴展RefactoringWizard 框架
Eclipse中的RefactoringWizard框架擴展了Eclipse的Wizard框架,關于Wizard框架的介紹可以在Eclipse的幫助系統中找到,這里我們僅從OO設計和架構的角度探討一下RefactoringWizard框架。
我們從Wizard相關的幾個類開始:
1. WizardPage類
WizardPage是一個包含了多個界面元素(比如文本框Text,按鈕Button)的一個界面組合部分。各個Page之間是獨立的,是可以動態加載的。WizardPage類的職責有:
·組合SWT界面元素,構造出一個界面頁。
·定義本身界面元素的操作行為。
在RefactoringWizard框架中預設了兩個通用的屬性頁:PreviewWizardPage和ErrorWizardPage。PreviewWizardPage類是用來預覽重構后的修改,對比代碼或其他資源的變化。ErrorWizardPage類是用來處理條件檢查及錯誤狀態通知的。我們只需擴展RefactoringWizard框架就可以自動獲取這兩項強大的功能。
2. Wizard類
一個Wizard就是一個裝載一系列WizardPage頁的容器,Wizard類的職責有:
·裝載一系列WizardPage,構造出一個復雜的界面。
·裝載領域類來處理具體業務邏輯。(在RefactoringWizard框架中這個類就是Refactoring類)
維護WizardPage頁之間以及頁與領域類之間的數據傳遞和狀態共享。(在這里要補充一點,其實在具體RefactoringWizard框架的實現中有專門的類來分擔這部分職責。)
我們的界面行為可以千變萬化(通過組合不同的WizardPage),而負責處理業務邏輯的領域類也可以獨立的變化,你可以隨意擴展Wizard的界面功能(-對擴展開放),而不用修改現有RefactoringWizard框架(-對修改封閉),這正是OO設計的最基本原則-OCP(Open-Close Principle)。
3. WizardDialog類
這個對話框類的主要職責是構造一個完整的GUI界面以及操作界面。它預設了一些按鈕(Back,Next,Finish,Cancel)等界面元素,它負責裝載Wizard類,操作時通過按鈕Back、Next來在多個WizardPage之間切換。
下面我們給出RefactoringWizard框架的架構圖:
![]() 圖 5 Refactoring Wizard架構圖 |
從圖 5中我們可以看到,如果我們把每一個WizardPage頁看作一項業務,那么Refactoring正是處理業務邏輯的控制中心,它封裝了所有對業務邏輯的處理,當然它可以在將處理任務委任出去。但請注意,它并不負責實現業務流程,也就是說各業務(各個Page界面)之間的邏輯順序關系不由它維護。
RefactoringWizard框架充分考慮到了應用的可擴展性,它在SWT的MVC(模型-視圖-控制)元架構模式的基礎上,添加了一些新的架構元素。MVC模式促使業務邏輯與界面分離,界面與控制行為分離,而RefactoringWizard框架增強了界面本身分離的特性,它將一個完整的界面分拆成多個頁面,用戶可以動態組合這些頁面或添加新的頁面來擴展界面行為。這種特性-界面的動態組合,低耦合,高內聚,封裝良好的接口-讓我們領略到了OO設計的精髓。
下面我們通過以下幾個步驟來擴展RefactoringWizard框架:
·擴展RefactoringWizardPage
·擴展RefactoringWizard
·啟動RefactoringWizard
第一步,擴展RefactoringWizardPage:首先我們新建一個類AnnotationRefactoringWizardPage,它需要繼承UserInputWizardPage類(其父類是RefactoringWizardPage,而RefactoringWizardPage最終實現了IDialogPage接口)。接下來就是實現IDialogPage接口的createControl(…)方法,在這個方法里實現你的界面行為,比如我們例子中的TimeOut文本框,代碼清單如下:
清單 14
| /** * create composite to add UI elements */ public void createControl(Composite parent) { // define UI Composite composite = new Composite(parent, SWT.NONE); GridLayout lay = new GridLayout(); lay.numColumns = 2; composite.setLayout(lay); btnCheck = new Button(composite, SWT.CHECK); btnCheck.setText("Add timeout parameter"); GridData gdBtnCheck = new GridData(); gdBtnCheck.horizontalSpan = 2; gdBtnCheck.horizontalAlignment = GridData.FILL; btnCheck.setLayoutData(gdBtnCheck); labName = new Label(composite, SWT.WRAP); labName.setText("TimeOut:"); GridData gdLabName = new GridData(); gdLabName.horizontalAlignment = GridData.BEGINNING; gdLabName.grabExcessHorizontalSpace = true; labName.setLayoutData(gdLabName); txtTimeOut = new Text(composite, SWT.SINGLE | SWT.BORDER); GridData gdTxtTimeOut = new GridData(); gdTxtTimeOut.horizontalAlignment = GridData.END; gdLabName.grabExcessHorizontalSpace = true; txtTimeOut.setLayoutData(gdTxtTimeOut); txtTimeOut.setText("500"); // init status labName.setEnabled(false); txtTimeOut.setEnabled(false); // add listener defineListener(); // 將composite納入框架的控制 setControl(composite); Dialog.applyDialogFont(composite); } |
在這里我們要特別注意的一點是在定義完我們的界面元素后,需要將自定義的Composite納入框架的控制,就是這行代碼:"setControl(composite);"
在我們處理完輸入數據檢查后進入下一頁面之前,我們需要設置頁面完成的狀態,以及傳遞輸入數據到領域類Refactoring。我們用以下代碼設置好頁面完成的狀態后,下個頁面ErrorWizardPage就會處理顯示邏輯:
清單 15
| private void notifyStatus(boolean valid, String message) { //設置錯誤信息 setErrorMessage(message); //設置頁面完成狀態 setPageComplete(valid); } |
傳遞輸入數據通過以下代碼來處理:
清單 16
| private void setRefactoring(boolean selection, String text) { AnnotationRefactoring refactoring = (AnnotationRefactoring) getRefactoring(); refactoring.setNeedTimeout(true); if(selection) { refactoring.setTimeout(Integer.valueOf(txtTimeOut.getText()).intValue()); } } |
其中getRefactoring()方法是繼承自RefactoringWizardPage的方法,由于我們的RefactoringWizard類裝載了RefactoringWizardPage和Refactoring類,這個方法是從RefactoringWizard類中獲得的,這里面用到了Observer設計模式。至此,我們完成RefactoringWizardPage的擴展。
第二步,擴展RefactoringWizard:首先我們新建一個類AnnotationRefactoringWizard,它需要繼承RefactoringWizard類,這個類中我們只需要加載定義好的AnnotationRefactoringWizardPage類和AnnotationRefactoring類,當然復雜的處理已經有RefactoringWizard框架處理好了。下面我們在構造函數中加載Refactoring類:
清單 17
| public AnnotationRefactoringWizard(Refactoring refactoring) { super(refactoring, WIZARD_BASED_USER_INTERFACE); } |
然后我們加載我們的AnnotationRefactoringWizardPage類,只需重載父類RefactoringWizard的addUserInputPages()方法就可以:
清單 18
| protected void addUserInputPages() { page = new AnnotationRefactoringWizardPage("refactor annotation"); addPage(page); } |
第三步,啟動RefactoringWizard。擴展好RefactoringWizard之后,就需要在用戶點擊菜單項或是按鈕時彈出這個對話框。RefactoringWizard最好使用RefactoringWizardOpenOperation類來打開(當然也可以用RefactoringWizardDialog)。RefactoringWizardOpenOperation首先進行重構的初始檢查,通過后才打開RefactoringWinzard對話框,否則就會打開錯誤對話框。前面完成創建插件工程時我們提到,彈出RefactoringWizard對話框的代碼應該放到響應菜單操作的類的run函數中。具體到本文工程中,就是把下面的代碼放到AnnotationManageAction的run函數中。這些代碼首先依次構造Refactoring和RefacoringWizard子類AnnotationRefactoring和AnnotationRefactoringWizard,并將AnnotationRefactoring的引用傳遞給AnnotationRefactoringWizard,然后用RefactoringWizardOpenOperation打開AnnotationRefactoringWizard,彈出向導對話框。
清單 19
| public void run(IAction action) { Shell shell = window.getShell(); AnnotationRefactoring refactor = new AnnotationRefactoring(select); AnnotationRefactoringWizard wizard = new AnnotationRefactoringWizard(refactor); RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); try { op.run(shell, "Inserting @Override Annotation"); } catch (InterruptedException e) { e.printStackTrace(); } } |
小結
在Eclipse中有效的利用重構能夠大大的減輕軟件開發人員的工作負擔,提高軟件的健壯性。然而,目前重構仍然處在一個工具缺乏的時代。以Eclipse為例,只有JDT提供的重構工具最為完善,而針對其他語言例如C++、Python等的開發環境,都缺乏對應的重構功能。 通過本文提供的方法,我們能夠有效的利用Eclipse中的重構框架創建新的重構,從而進一步提高已有開發環境的效率。



