首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

擴展 Eclipse JDT:打造自訂方法可視性重構外掛完整教學

     編輯:Java 相關知識

動機:解決 Java 開發中的可視性管理痛點

  • 初期開發常將方法設為 `public`,後期重構時需逐一調整為 `private`/`protected`/package-private
  • Eclipse 內建重構僅支援單一方法,無法**批量修改多個方法可視性**
  • Outline 視圖、Package Explorer、Hierarchy 視圖中右鍵無「修改可視性」選項
  • 手動編輯修飾符易出錯、無預覽、無撤銷支援

Eclipse 擴展點架構核心概念

概念XML 標籤角色
擴展點``宣告插件提供的「插座」,供其他插件插入功能
擴展``使用他人擴展點,「插頭」端,指定 `point` 屬性
插件`plugin.xml`功能單元,含擴展點宣告與擴展實現
插件註冊表啟動時建立內存中全域表,記錄所有擴展點與擴展關係

核心擴展點:org.eclipse.ui.popupMenus

向**彈出選單**貢獻選單項,針對特定物件類型 (透過 `objectClass` 過濾)。

<extension point="org.eclipse.ui.popupMenus">
    <objectContribution
        objectClass="org.eclipse.jdt.core.IMember"
        id="com.example.jdt.modifiers">
        
        <menu
            label="Soln: Modifiers"
            path="group.reorganize"
            id="com.example.jdt.modifiers.menu">
            <separator name="group1"/>
            <separator name="group2"/>
        </menu>
        
        <action
            label="Private"
            menubarPath="com.example.jdt.modifiers.menu/group1"
            class="com.example.jdt.actions.MakePrivateAction"
            id="com.example.jdt.modifiers.makePrivate"/>
            
        <action
            label="Protected"
            menubarPath="com.example.jdt.modifiers.menu/group1"
            class="com.example.jdt.actions.MakeProtectedAction"
            id="com.example.jdt.modifiers.makeProtected"/>
            
        <action
            label="Public"
            menubarPath="com.example.jdt.modifiers.menu/group1"
            class="com.example.jdt.actions.MakePublicAction"
            id="com.example.jdt.modifiers.makePublic"/>
            
        <action
            label="Package (Default)"
            menubarPath="com.example.jdt.modifiers.menu/group1"
            class="com.example.jdt.actions.MakePackageAction"
            id="com.example.jdt.modifiers.makePackage"/>
            
        <action
            label="Toggle Final"
            menubarPath="com.example.jdt.modifiers.menu/group2"
            class="com.example.jdt.actions.ToggleFinalAction"
            id="com.example.jdt.modifiers.toggleFinal"/>
            
    </objectContribution>
</extension>

關鍵設計決策

  • `objectClass="org.eclipse.jdt.core.IMember"`:**精準鎖定 Java 成員** (方法、欄位),在 Outline、Package Explorer、Hierarchy、Open Type Hierarchy 等所有視圖生效
  • `path="group.reorganize"`:插入到現有「重構」選單群組中,符合使用者預期
  • 分組 `group1` (可視性) / `group2` (final) 邏輯清晰
  • `class` 指向具體 `IObjectActionDelegate` 實現類

選中事件處理:ISelectionChangedListener

Action 類實作 `IObjectActionDelegate`,自動接收選中變更通知:

public abstract class AbstractModifierAction implements IObjectActionDelegate {
    protected IMember[] selectedMembers = new IMember[0];
    
    @Override
    public void selectionChanged(IAction action, ISelection selection) {
        if (selection instanceof IStructuredSelection) {
            IStructuredSelection ss = (IStructuredSelection) selection;
            List<IMember> members = new ArrayList<>();
            for (Object obj : ss.toArray()) {
                if (obj instanceof IMember) {
                    members.add((IMember) obj);
                }
            }
            this.selectedMembers = members.toArray(new IMember[0]);
        } else {
            this.selectedMembers = new IMember[0];
        }
        // 更新選單項啟用狀態
        action.setEnabled(canExecute());
    }
    
    protected boolean canExecute() {
        return selectedMembers.length > 0;
    }
    
    @Override
    public void run(IAction action) {
        for (IMember member : selectedMembers) {
            if (isApplicable(member)) {
                modifyMember(member);
            }
        }
    }
    
    protected abstract boolean isApplicable(IMember member);
    protected abstract void modifyMember(IMember member);
}

核心難點:程式化修改 Java 原始碼 (JDT AST vs Scanner)

方案選擇:IScanner (輕量、針對性強)

修改方法修飾符屬於**局部詞法修改**,不需完整 AST 解析。`IScanner` (詞法掃描器) 完美勝任:

  • 將原始碼切分為 Token (關鍵字、標識符、運算符、字面值...)
  • 定位方法宣告區域 (`IMember.getSourceRange()`)
  • 掃描 Token 找到修飾符關鍵字 (`public`、`private`、`protected`、`final`)
  • 用 `IBuffer.replace()` 替換/插入/刪除 Token
  • 呼叫 `ICompilationUnit.reconcile()` 同步模型

核心修改邏輯:以 public → private 為例

public void modifyMember(IMember member) {
    ICompilationUnit cu = member.getCompilationUnit();
    if (!cu.isWorkingCopy()) return;  // 僅支援編輯器中開啟的工作副本
    
    try {
        IBuffer buffer = cu.getBuffer();
        ISourceRange range = member.getSourceRange();  // 方法在源碼中的範圍
        
        // 建立掃描器,限制掃描範圍
        IScanner scanner = ToolFactory.createScanner(false, false, false, false);
        scanner.setSource(buffer.getCharacters());
        scanner.resetTo(range.getOffset(), range.getOffset() + range.getLength() - 1);
        
        // 掃描至左括號 '(' 為止 (方法簽名結束)
        int token = scanner.getNextToken();
        while (token != ITerminalSymbols.TokenNameEOF 
               && token != ITerminalSymbols.TokenNameLPAREN) {
            
            if (token == ITerminalSymbols.TokenNamePUBLIC) {
                // 找到 public,替換為 private
                buffer.replace(
                    scanner.getCurrentTokenStartPosition(),
                    scanner.getCurrentTokenEndPosition(),
                    "private"
                );
                break;  // 方法只有一個可視性修飾符
            }
            // 同理處理 private、protected...
            token = scanner.getNextToken();
        }
        
        cu.reconcile(ICompilationUnit.NO_AST, false, null, null);  // 同步模型、刷新視圖
        
    } catch (JavaModelException | BadLocationException e) {
        // 錯誤處理
    }
}

處理 final 切換邏輯

// 掃描過程中記錄 final 位置
int finalTokenPos = -1;
while (token != ITerminalSymbols.TokenNameEOF && token != ITerminalSymbols.TokenNameLPAREN) {
    if (token == ITerminalSymbols.TokenNameFINAL) {
        finalTokenPos = scanner.getCurrentTokenStartPosition();
    }
    token = scanner.getNextToken();
}

if (finalTokenPos >= 0) {
    // 有 final -> 移除
    buffer.replace(finalTokenPos, finalTokenPos + "final".length(), "");
} else {
    // 無 final -> 在可視性修飾符後插入 (或開頭)
    int insertPos = findInsertPosition(scanner, buffer);
    buffer.replace(insertPos, insertPos, "final ");
}

完整 Action 實現結構

// 抽象基類
public abstract class AbstractModifierAction implements IObjectActionDelegate { ... }

// 具體實現
public class MakePrivateAction extends AbstractModifierAction {
    protected void modifyMember(IMember member) {
        changeVisibility(member, Flags.AccPrivate);
    }
}

public class MakeProtectedAction extends AbstractModifierAction {
    protected void modifyMember(IMember member) {
        changeVisibility(member, Flags.AccProtected);
    }
}

public class MakePublicAction extends AbstractModifierAction {
    protected void modifyMember(IMember member) {
        changeVisibility(member, Flags.AccPublic);
    }
}

public class MakePackageAction extends AbstractModifierAction {
    protected void modifyMember(IMember member) {
        changeVisibility(member, 0);  // 移除所有可視性修飾符
    }
}

public class ToggleFinalAction extends AbstractModifierAction {
    protected void modifyMember(IMember member) {
        toggleFinal(member);
    }
}

開發環境搭建與除錯流程

  1. **新建 Plug-in Project**:`File > New > Project > Plug-in Development > Plug-in Project`
  2. **專案名**:`com.example.jdt.modifiers` (慣例:專案名 = 插件 ID)
  3. **目標平台**:`Eclipse Version` (當前執行的 Eclipse)
  4. **範本**:`Hello, World` (稍後大幅修改)
  5. **啟動 Runtime Workbench**:`Run > Run As > Eclipse Application` → 新 Eclipse 實例載入你的插件
  6. **除錯技巧**:
    • `System.out.println` 輸出到開發實例 Console
    • 斷點除錯:`Run > Debug As > Eclipse Application`
    • 修改代碼後熱插拔:儲存 → Runtime Workbench 自動重載 (或手動 `Reload`)

進階議題與最佳實踐

1. 支援 .class 檔案 (二進位成員)

  • `BinaryMember` 不支援 `getBuffer()`/`reconcile()`
  • 需反編譯、修改、重編譯、替換 .class —— **極其複雜,建議明確不支援**,UI 上禁用選單項
  • `selectionChanged` 中檢查 `member instanceof SourceMember` 或 `cu.isWorkingCopy()`

2. 批量修改支援

  • `run()` 中迭代 `selectedMembers` 逐一修改
  • 單次 `reconcile()` 可批量提交,減少視圖閃爍

3. 撤銷/重做支援

  • JDT `ICompilationUnit.reconcile()` 內建支援 Eclipse 撤銷堆疊
  • 用戶可 `Ctrl+Z` 撤銷修飾符變更

4. 快捷鍵綁定

<extension point="org.eclipse.ui.bindings">
    <key sequence="Ctrl+Shift+M" 
         contextId="org.eclipse.jdt.ui.javaEditorScope"
         commandId="com.example.jdt.modifiers.makePrivate"
         schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"/>
</extension>

5. 進階重構:使用 AST (Abstract Syntax Tree)

  • 適用於:複雜重構 (移動方法、提取介面、重命名參數...)
  • `ASTParser` 建立 AST → `ASTRewrite` 記錄修改 → `rewrite()` 套用
  • 修飾符修改屬於「殺雞用牛刀」,Scanner 更輕量精準

總結:從需求到插件的完整路徑

  1. 識別痛點:批量修改方法可視性無工具支援
  2. 選擇切入點:`org.eclipse.ui.popupMenus` + `IMember` 物件貢獻
  3. 掌握核心 API:`IScanner` 詞法掃描 + `IBuffer` 文字替換 + `reconcile()` 模型同步
  4. 處理邊界情況:僅支援工作副本、二進位成員禁用、多選批量處理
  5. 打磨使用者體驗:選單分組、圖示、快捷鍵、啟用狀態動態更新
  6. 驗證與發佈:Runtime Workbench 測試 → Export Deployable Plugin → 團隊分享/Marketplace 發佈

這個插件雖小,却完整展示了 **Eclipse JDT 擴展開發的核心技術棧**:擴展點機制、JDT Java 模型 (IMember, ICompilationUnit)、詞法掃描器 (IScanner)、編輯器緩衝區 (IBuffer)、模型協調 (reconcile)。掌握這套技術,你就能為 Eclipse 打造任何針對 Java 代碼的客製化重構、分析、生成工具——**幾十行代碼,改變日常開發體驗**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved