程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 擴展Eclipse的Java開發工具

擴展Eclipse的Java開發工具

編輯:關於JAVA

由於 Eclipse 具有功能強大的 Java 開發環境,這使它獲得了人們的一致好評。這個 Java 開發環境(再加上團隊環境和其它基礎功能)使 Eclipse 成為引人注目的集成開發環境,對 Java 開發人員來說,這是個好消息。而且,Eclipse 還是一個開放源碼項目。但真正使人們對 Eclipse 感到興奮的是它提供了擴展的可能性。

許多基於 Eclipse 的商用產品都顯示出這種提供集成產品的方法的實際意義。例如,IBM WebSphere Application Developer 和 Rational XDE 就說明了 Eclipse 已具有的影響。這些產品和其它基於 Eclipse 的產品降低了用戶的“學習曲線”,因為它們具有類似的用戶界面。當然,對於大型軟件公司來說,這頗有價值,但是對於小公司或個人有什麼用嗎?

這正是 Eclipse 擴展性能力讓人感興趣的地方。不僅那些擁有大型開發組織的公司可以用它進行集成,任何願意花些時間學習幾個 Eclipse 框架的人也都可以利用這一能力。“哦不,”您可能在想,“別再提什麼框架;我沒時間學習更多框架。”不必擔心;這學起來很快而且相當容易。在您的另一絲疑慮在頭腦中開始形成之前,先聲明一點,本文絕不是對 Eclipse 進行毫無價值的“hello world”式擴展。請放心,在如何增強 Eclipse 的 Java 開發環境的生產性使用方面,您將看到實際的價值以及一個清晰演示。您甚至還可能有點驚奇地發現:要完成某些相當令人驚異的事情,只要幾十行代碼就可以了。

本文將向您展示什麼是可能的,從哪裡開始,並將向您提供開始時需要什麼的可靠評價。盡管擴展 Eclipse 是一個高級主題,但是您只要先了解如何使用 Eclipse 的 Java 開發環境就可以了。

自己輕松重構成員可視性

最初在編寫代碼時,我沒有過多地擔心將方法可視性歸為 default(包)、private、public 還是 protected。在創建方法時,我使它們都為 public。只有當我最後定下了包的組織結構並完成了對方法的重構 -不管是通過從現有代碼抽取出新方法、在層次結構中上移或下移方法還是將方法整個地移至其它類中 -我才會重新檢查方法的可視性。我認為,在我知道最終類的模樣並實際使用過代碼之前,我不想聲明我的“客戶們”可能需要什麼。換句話說,在共享新框架之前,必須確定什麼是實現細節,什麼是必需的,這樣別人才能夠擴展它。

如果只需在 Outline 視圖、Hierarchy 視圖或任何您查看方法的地方選擇方法 -然後通過單擊菜單選項,就可以將一個或多個方法設置成所期望的可視性 - 那麼這會非常方便。誠然,我習慣了在我使用 VisualAge for Smalltalk 那段日子裡學到的這一功能。圖 1 顯示了對 Eclipse 的 Java 開發環境中 Java 編輯器的 Outline 視圖上下文的擴展。

圖 1. 對方法的上下文菜單進行的擴展

從用戶的角度而言,這很巧妙,因為這是引入用戶界面的很自然的方法。沒有任何暗示說這些新的菜單選項不屬於 Eclipse 最初的 Java 開發工具(Java Development Tool,JDT)。事實上,那就是菜單級聯使用“soln”前綴的原因 -這樣您就可以分辨出它是一個擴展!而且,開發人員不必記住只有在特定視圖或編輯器中才可以使用這些選項,因為只要方法顯示在哪裡,它們就可以在哪裡顯示。

簡述“Hello World”

“嘿,等一下,您承諾過不會有‘Hello, World’的!”是的,但在我們開始討論真正有趣的事情之前,確實需要先來了解一下 Eclipse 的基礎。所以,如果您從未編寫過自己的 Eclipse 擴展,那麼請和我一起快速了解一下 Eclipse 的體系結構和插件開發環境。

本質上,Eclipse 是一組松散綁定但互相連接的代碼塊。如果搞清楚這些代碼塊如何被“發現”,以及它們之間怎樣相互發現和擴展,就能了解 Eclipse 體系結構的基本原理。

圖 2. Eclipse 平台體系結構

擴展 vs 擴展點

要知道這兩者的 XML 標記十分相似。 擴展點 聲明插件的功能對於其它插件的可用性,它用 <extension-point> 標記表示。 擴展 使用以前定義好的擴展點,用 <extension> 標記表示,該標記使用 point 屬性來指定它希望使用的擴展點。

這些功能單元稱為 插件。Eclipse 平台運行時(參見圖 2)負責在名為 plugin.xml 的文件中查找這些插件的聲明(稱為 插件清單),每個 plugin.xml 文件都在各插件的子目錄中,這些子目錄位於 Eclipse 的安裝目錄下名為 plugins 的公共目錄(具體而言,就是 <inst_dir>\eclipse\plugins)。根據這些文件,Eclipse 平台運行時就在啟動時在內存中構建一個全局注冊表,稱為 插件注冊表,根據這個注冊表,給定的插件就可以在運行時確定其它哪些插件希望擴展它。希望被其它插件擴展的插件將聲明一個 擴展點。這是插件的某種“電源板”,通過對插件聲明 擴展,其它插件就可以利用這個插件。

回到我們的示例,那麼任務就是通過查找滿足我們需要的相應擴展點來決定從哪裡“插入” Eclipse。幸運的是,一旦使用 Eclipse 一段時間後,您就會知道有數量驚人的東西可以使用,盡管可能您還沒有實際使用過。這是因為您在 Eclipse 用戶界面所看到的與由構成 Eclipse 插件的類所建的模型通常幾乎是一一對應的。圖 3 使這一點更為清晰:

圖 3. 視圖及其模型

這裡我們看到一系列普通的用戶界面,右側的是最廣為人知的用戶界面 - 命令提示符(Command Prompt)窗口,在其中用 dir 命令顯示文件系統內容,然後是左邊專門化程度較高的視圖 - JDT 的 Package Explorer。從用戶界面的角度來看,所有這些視圖都將同一“模型”(也就是一些文件)的表示可視化。作為 Eclipse 用戶,我們很自然地會希望這兩個 Eclipse 視圖同時向我們提供查看同一事物的不同方法:Navigator 展示了部分操作系統文件的專門化視圖(Eclipse 的工作空間),而 Package Explorer 向我們展示了同樣的一些文件,這些文件是用對 Java 程序員而言更自然更高效的方法組織和表示的。

通過了解 Eclipse 用戶界面如何反映其底層模型,以及 Eclipse 模型如何成為相互構建的基礎,這向我們提供了該如何找到“插入”我們擴展的最佳位置的重要線索。顯示在視圖下面的 Eclipse 接口名稱 IFile 和 ICompilationUnit 就是我們可以預期從構成 Eclipse 的模型中獲得的兩個接口示例。由於它們通常對應於用戶界面中顯示的控件項,所以您已經對通過編程獲得的效果有一個直觀的感受。

這是我們“旅行”的第 I 部分。在第 II 部分中,我們將討論開發解決方案。我們不打算提供這個解決方案並逐一解釋,探索其中的一些奧秘,這不是更有趣嗎?讓我們首先討論與以下這個問題相關的一些問題:使用我們自己的方法可視性重構能力來擴展 JDT。

把問題問在點子上比知道答案更重要

我們先探討一些常規問題:

在用戶界面中,如何顯示擴展,以及顯示在何處?

通常如何擴展用戶界面?

對用戶界面的擴展如何知道類似於“選擇”這樣的基本事件?

我們對基本 Eclipse 領域有了很好的了解之後,將轉向一些特定於 JDT 的問題:

如何擴展 JDT 的特定元素的用戶界面(象 Outline 視圖中顯示的成員)?擴展視圖還是它們的底層模型?

Package Explorer 中顯示的元素和其它視圖(如 Outline 視圖)中顯示的相同元素之間有什麼關系?我們的擴展需要知道它們之間的任何區別嗎?

如何通過編程更改 JDT 模型?

怎樣分析 Java 源代碼以進行修改?

當然,還有最後一個大問題:

下一步是什麼?

在用戶界面中,如何顯示擴展,顯示在何處?

這在很大程度上是一個溫和的提示,因為我們已得到了答案。我們希望對一個或多個選中的方法顯示上下文菜單選項,這些菜單選項允許我們只用一個操作就可以更改方法的可視性。我們更喜歡在可以顯示方法的任何地方都能使用這些菜單選項,如在 Hierarchy 視圖和 Package Explorer 中。這把我們帶到下一個問題。

通常如何擴展用戶界面?

通過示例來學習會更有趣,這方面 Plug-in Project 向導可以提供幫助,它提供了一些樣本代碼,我們可以修改這些代碼來滿足我們的需要。我們將回答該向導中的幾個問題,它將自動啟動用於插件開發的專門透視圖,稱為 Plug-in Development Environment(PDE),以准備測試。該向導包含了可以幫助我們入手的許多示例。事實上,我們的老朋友“Hello World”也在那裡。為了沿襲這個傳統,我們將生成這個“Hello World”,查看結果以驗證是否正確安裝了該環境,隨後修改它以幫助我們回答當前的問題,並把我們帶到下一個問題: 對用戶界面的擴展如何知道類似於“選擇”這樣的基本事件?這很重要,因為我們希望將我們新近引入的菜單選項應用到當前選中的方法上。

請注意,這些指示信息假定您正從全新的 Eclipse 安裝開始。如果修改了該環境或更改了首選項,那麼可能不會完全象如下所述那樣工作。您可以考慮從全新的工作空間啟動 Eclipse:打開命令提示符窗口,更改到 <inst_dir>\eclipse 目錄,然後使用 -data 參數啟動 Eclipse,如清單 1 所示。

清單 1. 啟動全新的 Eclipse 實例

cd c:\eclipse2.1\eclipse
eclipse.exe -data workspaceDevWorks

從使用 New Plug-in Project 向導創建一個插件項目開始。選擇 File > New > Project。在 New Project 對話框中,在向導列表中選擇 Plug-in Development and Plug-in Project,然後選擇 Next。將項目命名為 com.ibm.lab.helloworld 。該向導將根據這個名稱創建插件標識,所以它在系統中必須是唯一的(按慣例,項目名和插件標識相同)。使用顯示在“Project contents”下面的推薦缺省工作空間位置就可以了;選擇 Next。

在下一頁上,選擇 Next以接受缺省插件項目結構。該插件代碼生成器頁推薦了許多樣本,向導可以幫助您進一步對該項目進行參數化。選擇“Hello, World”選項,然後選擇 Next。下一頁(顯示在圖 4 中)推薦了插件名和插件類名。這些名稱基於插件項目 com.ibm.lab.helloworld 的最後一個單詞。這個示例不需要任何插件類便利方法,所以取消對三個代碼生成選項的選擇(如圖 4 所示),然後選擇 Next(不是 Finish;您還有一頁要完成)。

圖 4. Simple Plug-in Content

您可以在下一頁(顯示在圖 5 中)中指定參數,這些參數對於“Hello, Worlds”示例是唯一的,比如,將要顯示的消息。

圖 5. Sample Action Set

要簡化所產生的代碼,將該操作的目標包名從 com.ibm.lab.helloworld.actions 更改成 com.ibm.lab.helloworld ,即與該項目的名稱相同。盡管在實際的插件中,您可以選擇用不同的包對相關的類進行分組,但在本例中,只有兩個類,所以不必這樣做。這樣也遵循了“主”包名和項目名相同這個慣例。現在選擇 Finish。

您應該看到一個信息消息:“Plug-ins required to compile Java classes in this plug-in are currently disabled. The wizard will enable them to avoid compile errors.”。選擇 OK 繼續。如果這是個全新的工作空間,那麼您還將看到另一個信息消息:“This kind of project is associated with the Plug-in Development Perspective. Do you want to switch to this perspective now?”。 選擇 Yes以根據這個消息的建議進行切換。

要驗證所有的東西是否都設置正確,讓我們測試新插件。選擇 Run > Run As > Run-Time Workbench。這將啟動 Eclipse 的第二個實例,它將包含您的插件。這個新實例將創建一個新的名為 runtime-workspace 的工作空間目錄,所以不必擔心;對這個實例所作的任何測試都不會影響開發設置。您應該看到類似圖 6 的樣子,其中有一個新的下拉菜單,其標簽為 Sample Menu,它有單一的選項 Sample Action。選擇它將顯示下面的信息消息。如果您不是從全新的工作空間啟動,那麼可以選擇 Window > Reset Perspective以查看新生成的下拉菜單;從現有工作空間啟動時不會顯示這個菜單,因為工作台“記得”上次 Eclipse 運行時哪些操作集是活動的(您還可以從 Window > Customize Perspective...下拉菜單選項上添加/刪除操作集)。

圖 6. Hello, Eclipse world

讓我們快速浏覽一下插件清單文件 plugin.xml。雙擊它,以在 Plug-in Manifest 編輯器中打開它。這個編輯器提供了幾個類似於向導的頁和一個“原始”源代碼頁。通過選擇 Source 選項卡轉到源代碼頁。您將看到與以下清單 2 顯示的代碼相似的內容;我們感興趣的是用粗體顯示的那幾部分。

清單 2. 所生成的“Hello, World” plugin.xml

<extension
   point="org.eclipse.ui.actionSets">>
  <actionSet
    label="Sample Action Set"
    visible="true"
    id="com.ibm.lab.helloworld.actionSet">
   <menu
     label="Sample &Menu"
     id="sampleMenu">
    <separator
     name="sampleGroup">
    </separator>
   </menu>
   <action
     label="&Sample Action"
     icon="icons/sample.gif"
     class="com.ibm.lab.helloworld.SampleAction"
     tooltip="Hello, Eclipse world"
     menubarPath="sampleMenu/sampleGroup"
     toolbarPath="sampleGroup"
     id="com.ibm.lab.helloworld.SampleAction">
   </action>
  </actionSet>
</extension>

並不需要過於深入地研究這個代碼。我們“旅行”的第 II 部分的目的只是讓您熟悉一些基本機制,借此我們可以介紹 JDT 的擴展。這裡,您會看到這樣一種技術的一個樣本:它將菜單和菜單選項作為操作集添加到工作台。它以一個用 <extension point="org.eclipse.ui.actionSets"> 標記聲明的擴展開始。工作台用戶界面插件定義了這個擴展點 org.eclipse.ui.actionSets ,以及幾個類似的擴展點,通過這幾個擴展點可以向各種用戶界面元素提供其它插件。

我們還未回答如何將菜單選項添加到 Java 方法的上下文菜單中。一個簡單示例可以給我們一些提示。首先打開顯示“Hello, World”消息的類 SampleAction ,請注意其 run 方法。它不是特別有趣;不過我們還看到了另一個方法 selectionChanged 。啊哈!下一個問題的答案有了。

對用戶界面的擴展如何知道類似於“選擇”這樣的基本事件?

工作台“選擇”更改時會告知所提供的操作(象我們提供的菜單下拉選項)。這在 selectionChanged 方法前面的 Javadoc 注釋中得到了確認。讓我們修改這個方法以告知有關“選擇”的更多信息。首先,如果您還沒有關閉工作台的運行時實例,那麼現在就關閉。然後對 selectionChanged 方法添加清單 3 中的代碼。

清單 3. selectionChanged 方法,首次修改

public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  System.out.println(selection);
}

有了這個調試代碼,我們將看到選擇了什麼,並了解到有關什麼使 Eclipse 工作的更多信息。保存該方法,然後重新啟動運行時工作台。

重要:Eclipse 有一個延遲裝入的策略,以在用戶執行需要插件代碼的操作時才裝入插件。所以您必須 先選擇 Sample Action 菜單選項,以在調用 selectionChanged 方法之前裝入您的插件。

現在選擇其它東西,如編輯器中的文本、Navigator 中的文件,當然還有 Outline 視圖中的成員(回憶一下:您必須創建一個 Java 項目和示例 Java 類來做到這一點,因為運行時實例使用不同的工作空間)。清單 4 顯示了您將在 Eclipse 的 開發實例的控制台中看到的某個示例輸出。

清單 4. selectionChanged 輸出,首次修改

==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java
   [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
org.eclipse.jface.text.TextSelection@9fca283
==========> selectionChanged
<empty selection>
==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java
   [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
[IMember[] members [in ChangeIMemberFlagAction [in [Working copy] ChangeIMemberFlagAction.java
   [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
[ChangeIMemberFlagAction.java [in com.ibm.lab.soln.jdt.excerpt
    [in src [in com.ibm.lab.soln.jdt.excerpt]]]
  package com.ibm.lab.soln.jdt.excerpt
  import org.eclipse.jdt.core.Flags
  import org.eclipse.jdt.core.IBuffer
  ...lines omitted...
   void selectionChanged(IAction, ISelection)]
==========> selectionChanged
[boolean isChecked(IAction, IMember) [in ToggleIMemberFinalAction
   [in ToggleIMemberFinalAction.java [in com.ibm.lab.soln.jdt.excerpt
   [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]

唔,這不如我們所希望的那麼有啟發性。很明顯,這個“選擇”不象 String 的實例那麼基本,但沒有明示涉及了什麼類,因為這些類很明顯覆蓋了它們的缺省 toString 方法。如果不用多做一點研究就可以明白這些類向我們顯示的信息,那我們就輕松了,但目前我們還沒有達到這種程度。回到 selectionChanged 方法,浏覽 selection 參數的接口 ISelection 的層次結構。其層次結構表明它的通用子類型接口並不多,只有 IStructuredSelection (用於列表)和 ITextSelection 。通過輸出所選的類,我們可以使 selectionChanged 方法稍微更智能一點。如清單 5 所示修改 selectionChanged 方法。

清單 5. selectionChanged 方法,第二次修改

public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  if (selection != null) {
   if (selection instanceof IStructuredSelection) {
    IStructuredSelection ss = (IStructuredSelection) selection;
    if (ss.isEmpty())
     System.out.println("<empty selection>");
    else
     System.out.println("First selected element is " + ss.getFirstElement().getClass());
   } else if (selection instanceof ITextSelection) {
    ITextSelection ts = (ITextSelection) selection;
    System.out.println("Selected text is <" + ts.getText() + ">");
   }
  } else {
   System.out.println("<empty selection>");
  }
}

同樣,請記住關閉運行時實例,然後重新啟動。現在當您選擇用戶界面的各種元素時,它們是不是提供了更多信息,如清單 6 所示。

清單 6. selectionChanged 輸出,第二次修改

----selected some methods in the Outline view
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
<selection is empty>
   activated the Java editor
==========> selectionChanged
Selected text is <isChecked>
==========> selectionChanged
<selection is empty>
   selected same methods and classes, package in the Package Explorer
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceType
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.PackageFragment
   activated the Navigator view, selected some files, folders, and projects
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
<selection is empty>
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Project
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Folder
==========> selectionChanged
<selection is empty>
   reactivated the Package Explorer,
   selected some classes and methods in JARs of reference libraries
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.JarPackageFragment
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.ClassFile
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod

特別地,我們確認在用戶界面中看到的東西與 JDT 模型類一一對應。我們之所以了解所顯示的是作為選擇的模型,而不是類似於字符串和圖像的較低級的基本類型,要歸功於另一個 Eclipse 框架,稱為 JFace。這個框架在象字符串這樣的基本類型(接近操作系統的窗口小部件希望使用這些基本類型)和更高級的模型對象(您的代碼更願意使用這些對象)之間進行映射。本文只是略微提及這個主題,因為我們設定的目標是擴展 JDT。本文將只討論理解 JDT 擴展的基礎所必需的知識。

回到輸出,特殊的選項結果引起了我們的注意:它們對應於用戶界面中 Java 成員的選擇。清單 7 中重復了它們。

清單 7. selectionChanged 輸出,再次研究

==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod

這些類的包名中間的 internal 使人有點擔心。但是,正如您通常會發現的,Eclipse 會有一個公共(public)接口,它對應於(內部)實現類,就如這裡的例子。快速類查找揭示出:這些類都實現了看來很有希望成為這個問題答案的一組公共接口,也就是 ISourceReference 、 IJavaElement ,尤其還有 IMember 。最後!現在我們可以開始早就希望進行的擴展,這使我們去回答下一個問題。

如何擴展 JDT 的特定元素的用戶界面(象 Outline 視圖中的成員)?擴展視圖還是它們的底層模型?

簡單的“Hello, World”示例顯示了添加菜單選項只需要在插件清單文件中添加幾行 XML( <extension point="org.eclipse.ui.actionSet"> )以及一個處理實際操作的類( com.ibm.lab.helloworld.SampleAction )。向視圖的下拉菜單、公共編輯器的工具欄以及彈出菜單添加操作基本上很簡單。所提供的彈出菜單歸結為兩類:一類只與視圖相關而與所選的對象無關(也就是,對視圖的“空白處”單擊鼠標右鍵通常會顯示的“缺省”彈出菜單),另一類則更常見,它們與應用於所選對象的選項相關。在我們的例子中,我們希望目標只是具體選擇的對象,所以通過在插件清單文件中定義一個擴展,我們將 提供的操作對象提供給這些對象的彈出菜單(對下面幾個標識符進行了縮寫,以獲得更佳的格式;用‘…’表示),如清單 8 所示。

清單 8. 修飾符操作

<extension point="org.eclipse.ui.popupMenus">
  <objectContribution
    objectClass="org.eclipse.jdt.core.IMember"
    id="...imember">

   <menu
     label="Soln: Modifiers"
     path="group.reorganize"
     id="...imember.modifiers">
    <separator name="group1"/>
    <separator name="group2"/>
   </menu>

   <action
    label="Private"
    menubarPath="...imember.modifiers/group1"
    class="...jdt.excerpt.MakeIMemberPrivateAction"
    id="...imember.makeprivate">
   </action>

   <action
    label="Protected"
    menubarPath="...imember.modifiers/group1"
    class="...jdt.excerpt.MakeIMemberProtectedAction"
    id="...imember.makeprotected">
   </action>

   ...all menu choices not shown...

  </objectContribution>
</extension>

擴展點命名為 org.eclipse.ui.popupMenus ,顧名思義,它定義了向出現在工作台中的彈出菜單提供的對象。這個特殊示例只提供給明確選擇的對象,即實現 IMember 接口的對象(請回憶一下 Java 語言規范中的定義,成員包含方法和字段)。我們的研究沒有白費;我們得到了當前問題的答案,我們差不多准備好回答下一個問題了。

在這樣做之前,此時請注意,我們找到的用於簡單“Hello, World”操作示例的模式將對所提供的其它菜單操作進行重復。即,將把選擇更改告知 class 屬性中指定的類(通過其 selectionChanged 方法),並還將告知它用戶何時選擇菜單選項(通過其 run 方法)。我們“旅行”的用戶界面部分快要結束了;更困難的部分,也是影響我們所期望更改的部分還在前面。正如下一個問題所說的那樣,在繼續之前只要做一兩次觀察。

Package Explorer 中顯示的元素和其它視圖(如 Outline 視圖)中顯示的相同元素之間有什麼關系?我們的擴展是否需要知道它們之間的任何區別?

您可能已經注意到,當您在 Outline 視圖和 Hierarchy 視圖中選擇了一個方法時,所選對象的類並非總是相同的。例如,如果您在 Package Explorer 中展開一個庫(JAR 文件)的內容,隨後選擇了一個類(或方法),那麼它也不會是 Java 編輯器的 Outline 視圖中同一選擇的那個類。到底怎麼回事?

這裡,我們正在觀察 JDT 的 Java 模型中“可編輯的”部分和始終為只讀的部分之間的差別。這兩部分 Java 模型都實現了公共接口(象 IMember ),但是它們擁有用來理解底層限制的實現類是不同的。另一個示例是,有一個表示 Java 編譯單元的實現類,它派生自 Package Explorer 所顯示的 JAR 文件的 .class 文件,還有另一個類表示直接派生自 .java 文件的編譯單元。後一個實現將允許進行一些前者所不允許的修改,而它們 API 的共享部分是由接口 ICompilationUnit 表示的。

您以前在編輯 Java 源代碼時,一定會觀察到:在您輸入方法特征符時 Outline 視圖進行了更新(如果您沒有注意到,那就試一下!)。這個示例說明了 JDT 如何在不同的區域暫放其“未提交的更改”,這與處理那些已保存、編譯和集成到 Java 模型中的更改不同。有些視圖(象 Java 編輯器的 Outline 視圖)知道未提交的更改和已提交的更改,而其它象 Navigator 這樣的視圖只關心已保存到文件系統的已提交更改。

隨後,我們所提供的用來修改 Java 成員的操作必須(至少在某種程度上)知道在什麼上下文中調用它。即,它必須識別出某些所選成員是可修改的(那些位於 Java 編輯器的 Outline 視圖中的成員),而另一些成員是不可以修改的(存儲在 JAR 文件中以及顯示在 Package Explorer 中的 .class 文件中的成員)。記住這一點,讓我們繼續下一個問題。

如何通過編程更改 JDT 模型?

如果您在前面的“旅行”中稍作了研究,那麼可能已經注意到 IMember 、 IJavaElement 以及我們的操作所看到的由所選的與 Java 相關的項實現的作為大部分接口的那部分都沒有 setXXX 方法。那麼如何修改這些接口呢?

您將發現這出奇地簡單,不過可能在直覺上不那麼明顯。JDT 的 Java 模型在大多數實踐情況下都是“只讀”的。通過與 Java 編譯器的集成協作,給定元素的底層 Java 源代碼進行的更改就與 Java 模型的其余部分的更改同步了。實際上,您要做的就是更新 Java 源代碼,而對模型所作的其余必要更改就傳送給任何依賴它們的元素中。例如,每當 Java 源代碼/Java 模型發生更改時,JDT 的索引會自動更新,所以仍舊可以快速執行搜索,重新編譯從屬類(按照項目特性中指定的 Java 構建路徑所指示的),等等。

可以大松一口氣了!以下就是 Java 模型是插件集成之關鍵的原因所在:Java 模型提供了整個 Java 環境的常見的內存中共享的模型,它的范圍從一個項目,一直到其所有被引用的庫,所有這些都不要您費心去操作文件系統中的 .java 文件、 .class 文件以及 .jar 文件。您可以將精力集中於高級模型,而讓 JDT 處理這其中的許多雜亂細節。

還不能確信它很容易?清單 9 包含了這個解決方案的核心代碼的一小部分,它是從提供操作的 run 方法上抽取出的,並出於可讀性考慮,稍作了簡化:

清單 9. selectionChanged 方法,小型解決方案

public void selectionChanged(IAction action, ISelection selection) {
  IMember member = (IMember)
    ((IStructuredSelection) selection).getFirstElement();
  ICompilationUnit cu = member.getCompilationUnit();
  if (cu.isWorkingCopy()) {
   IBuffer buffer = cu.getBuffer();
   buffer.replace(...);
   cu.reconcile();
  }
}

似乎有點虎頭蛇尾,不是嗎?對您提供的操作提供了選中的成員,向它請求其父容器(Java .class 或 .java 文件的模型,用 JDT 的說法,全都稱為 編譯單元),因為其父容器管理底層源代碼,驗證該成員是否屬於“未提交的” Java 模型(換句話說,它目前在編輯器中是打開的),然後修改作為緩沖器返回的源代碼。 IBuffer 接口類似於 StringBuffer ,其原理不同之處在於,更改與編譯單元相關的緩沖區更新了 Java 模型的對應元素。對 reconcile 的最終調用告知 JDT 去通知其它相關各方(象 Package Explorer 視圖):您的模型更新已准備好作為公共消費品。

您一定注意到上述代碼中的省略號。在那裡,您必須分析源代碼本身以進行修改。同樣,JDT 會提供幫助,正如我們將在下一個問題中看到的。

怎樣分析 Java 代碼以進行修改?

JDT 提供了幾個工具來幫助您分析代碼。本文有意選擇了最簡單的 IScanner 接口進行演示,它的作用域也最有限。這個接口屬於 JDT 工具箱,可以通過 JDT 的 ToolFactory 類訪問它。其 createScanner 方法返回一個掃描程序,該掃描程序會簡化對一串 Java 代碼作標記的工作。它不處理任何特別困難的操作,只是對所返回的標記進行簡單的解析和分類。例如,它指出下一個標記是 public 關鍵字,其後的標記是一個標識符,再後面的標記是左圓括號,等等。隨後,只有當您希望分析一小段代碼(您明確理解想要在這段代碼中得到什麼)時,這個掃描程序才是合適的。您決不會使用掃描程序分析整個 Java 源代碼;因為您會轉而使用一些對編譯器迷而言十分熟悉的工具:JDT 的抽象語法樹(Abstract Syntax Tree,AST)框架。

與簡單的掃描程序不同,AST 理解語言元素(它們不再只是“標記”)之間的關系。它可以識別象局部變量、實例變量、表達式以及 if 語句等六十多種不同的語言元素。它將幫助您進行涉及范圍廣泛的重構,或難以滿足對標記進行一對一分類的模糊程度特別高的重構。要更清晰地了解何時使用掃描程序與何時使用 AST 之間的差別,請考慮清單 10 中的代碼。

清單 10. 模糊的變量引用

public class Foo {
  int foo = 1;

  public int foo(int foo) {
    return foo + this.foo;
  }

  public int getFoo() {
   return foo;
  }
}

如果作為重構的一部分,您希望查找對實例變量 foo 的引用,那麼就會明白一個單純的解析會使區分本地引用和實例變量引用成為一個難題。AST 創建了完整的分析樹,其中表示了 Java 源代碼的每個元素並對這些元素進行了區分。在這個特例中,不同的類會考慮“foo”引用的上下文,將“foo”引用表示成 AST 的節點(如 FieldDeclaration 、 SimpleName 和 ThisExpression ),因此您會很輕松地識別它們。

正如前面提到的,本文將只討論我們所選擇的簡單例子。現在,讓我們回到我們前面跳過的用省略號表示的代碼。這個代碼將使用 IScanner 的實例以確定並替換源代碼中確定成員可視性的關鍵字。我們將處理的可視性修飾符是 public 、 private 、 protected 和 final 。通過采用“蠻力”方法,我們可以簡化這個解決方案,即,采用兩個步驟就可以完成。首先刪除方法特征符中所有的可視性修飾符(或至少掃描查找它們,如果找到,就刪除),然後插入所希望的修飾符。特別地:

如果在方法特征符中找到 public 、 private 或 protected ,就刪除它們。

插入所請求的可視性修飾符(對於包可視性的情況,不作任何操作,因為這是缺省操作;即沒有任何修飾符)。

final 修飾符很簡單。因為所希望的行為就是插入和除去這個修飾符,所以如果它存在,我們除去它;否則就插入它。清單 11 中的代碼只顯示了一個例子,它無條件地將成員的可視性從 pubilc 改成 private。在與本文相關的解決方案中,您將看到每個操作的公共代碼都被移到了抽象超類中。它基本上與下面的代碼相同,只不過稍作了整理以避免冗余。

清單 11. 掃描是否有 pubilc 關鍵字

ICompilationUnit cu = member.getCompilationUnit();
if (cu.isWorkingCopy()) {
  IBuffer buffer = cu.getBuffer();

  IScanner scanner = 
   ToolFactory.createScanner(false, false, false, false);
  scanner.setSource(buffer.getCharacters());
  ISourceRange sr = member.getSourceRange();
  scanner.resetTo(
   sr.getOffset(),
   sr.getOffset() + sr.getLength() - 1);
  int token = scanner.getNextToken();
  while (token != ITerminalSymbols.TokenNameEOF
    && token != ITerminalSymbols.TokenNameLPAREN)
   token = scanner.getNextToken();
   if (token == ITerminalSymbols.TokenNamePUBLIC) {
    buffer.replace(
     scanner.getCurrentTokenStartPosition(),
     scanner.getCurrentTokenEndPosition(),
     scanner.getCurrentTokenStartPosition() + 1,
     "private");
    break;
   }
  }
  cu.reconcile();
}

注: ITerminalSymbols 定義了掃描程序可以返回的標記名稱,它們對應於 Java 語法的標准標記。您可以進一步查詢掃描程序以詢問當前標記在緩沖區中開始和結束的具體位置,它出現在哪一行上,當然還有標記本身(特別是象 ITerminalSymbols.TokenNameStringLiteral 和 ITerminalSymbols.TokenNameIdentifier 這樣的例子,它們不是保留的關鍵字)。

上述代碼片段中,向 scanner.setSource 方法提供了編譯單元的完整源代碼,即 Java 源文件中的所有內容。正如前面提到的,掃描程序並不非常適合於大型分析,所以我們必須將它限制用於只有以目標方法的第一個字符開始,一直到調用 setSourceRange 方法作為結束的那部分源代碼。 IMember 接口繼承了 ISourceReference , ISourceReference 是一個允許您查詢包含編譯單元內的源代碼字符串和源代碼位置的接口。這使我們不必確定目標方法在 Java 源代碼內開始和結束的位置。原本可以用 AST 實現這一點,而 ISourceReference 接口使 AST 成了多余的工具。由於 Java 方法特征符易於解析,所以 IScanner 接口的解析能力和它很匹配。我們必須做的就是查找 public 關鍵字,它出現在方法聲明的前一個字符之後,參數聲明的左圓括號之前,用 private 關鍵字替換它。當然,在該解決方案中,這個接口將處理所有的可能情況,不管該方法最初是 public、private、protected 還是 package(缺省)。

下一步是什麼?

本文設定的目標是向您提供一個對 Eclipse 的 Java 開發環境頗具價值的擴展,這樣的擴展增強了這個開發環境的生產率。坦率地說,出於簡潔性考慮,我多次跳過了一些細節。該解決方案本身就作了一些簡化假設,象只允許在編輯器中對已打開的 Java 源代碼進行修改。您可能希望在更完整的實現中取消這個限制。

雖然如此,但我還是希望您能感受到什麼是可能的,並確信這樣做不是特別困難。本文中我們討論的是 The Java Developer's Guide to Eclipse 一書某一高級章節的部分內容。該書中有十一個比較淺顯的章節討論了插件開發的基礎。象本文一樣,大多數章節都包含了一個已文檔化的工作解決方案,它可以強化您所學到的知識,大多數內容是以本文中您已看到的相同風格編寫的(不過可能沒有以這麼快的節奏進行討論!)。

了解有關解決方案的更多信息並下載它

在解決方案摘錄中獲得本文所涉及內容的更多細節。該解決方案摘錄還描述了其它幾個有用的 JDT 擴展,它們包含在 The Java Developer's Guide to Eclipse 所附帶的光盤中。要安裝解決方案摘錄,先下載它,將包含在其中的項目解壓縮到您的工作空間中(例如, c:\eclipse2.1\eclipse\workspace ),然後通過選擇 File > Import > Existing Project into Workspace,將該項目導入到當前的 Eclipse 工作空間。

重要:您可能需要向工作空間添加必要的插件,這樣解決方案才能編譯和運行。選擇 Window > Preferences > Plug-in Development > Target Platform,然後選擇 Not in Workspace。這將確保解決方案所依賴的基礎插件在導入和重新編譯過程中可用。

一旦導入完成,您可能需要切換至 Plug-in Development 透視圖,在 com.ibm.lab.soln.jdt.excerpt 項目中選擇 plugin.xml ,然後選擇 Update Classpath。這將修改由於 Eclipse 安裝路徑和解決方案的安裝路徑不同所引起的編譯錯誤。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved