寫了個IDEA開源插件,解決讓人頭疼的 vo2dto

語言: CN / TW / HK

作者:小傅哥
博客:http://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!😄

頭炸,po2vovo2dodo2dto,一堆對象屬性,取出來塞進來。要不是為了 DDD 架構下的各個分層防腐,真想一竿子懟下去

那上 BeanUtils.copyProperties 呀,其實對象轉換不只這個方法,還有同類的12種手段,但綜合來看還是 MapStruct 在編譯期生成x.set(y.get)代碼的最終效果最好,整體壓測數據如下:

  • BeanUtils.copyProperties 是大家代碼裏最常出現的工具類,但只要你不把它用錯成 Apache 包下的,而是使用 Spring 提供的,就基本還不會對性能造成多大影響。
  • 但如果説性能更好,可替代手動get、set的,還是 MapStruct 更好用,因為它本身就是在編譯期生成get、set代碼,和我們寫get、set一樣。
  • 其他一些組件包主要基於 AOPASMCGlib,的技術手段實現的,所以也會有相應的性能損耗。

咋辦? 給每一個轉換對象屬性的操作都寫一個 MapStruct 嗎?也不合適呀,有些就是方法中很簡單的操作一下,寫寫代碼就能搞定,問題就是懶的寫,一多了還容易寫錯。別提 BeanUtils.copyProperties 有時候確定有性能問題,從編碼上還看不出來屬性的添加和減少

所以 我要寫個 IDEA Plugin 解決這個問題,目的就一個,通過 IDEA 插件開發能力,定義到我需要轉換屬性的2個對象,把2個對象的轉換代碼自動生成出來,並織入到我的對象定位位置上。

設計一個插件

我是這麼思考的:在 IDEA 開發工程代碼中,在需要轉換的2個對象間,複製第一個對象和屬性,再把光標定位到轉換對象上,接下來我給它提供個按鈕或者快捷鍵,一點就把所有轉換代碼生成出來,這樣不就解決了需要手寫的問題了嗎,效果如下:

1. 工程結構

java vo2dto ├── .gradle └── src ├── main │ └── java │ └── cn.bugstack.guide.idea.plugin │ ├── action │ │ └── Vo2DtoGenerateAction.java │ ├── application │ │ └── IGenerateVo2Dto.java │ ├── domain │ │ ├── model │ │ │ ├── GenerateContext.java │ │ │ ├── GetObjConfigDO.java │ │ │ └── SetObjConfigDO.java │ │ └── service │ │ ├── impl │ │ │ └── GenerateVo2DtoImpl.java │ │ └── AbstractGenerateVo2Dto.java │ └── infrastructure │ └── Utils.java ├── resources │ └── META-INF │ └── plugin.xml ├── build.gradle └── gradle.properties

源碼獲取http://github.com/fuzhengwei/vo2dto - 歡迎提交 issue、PR 共同維護

在此 IDEA 插件工程中,主要分為4塊區域:

  • action:提供菜單欄窗體,在插件中我們把這個菜單欄配置到 Generate 下,也就是通常你生成 get、set、constructor 方法的地方。
  • application:應用層定義接口,這裏定義了一個用於生成代碼並織入到錨點的方法接口。
  • domian:領域層專門處理代碼的生成和織入動作,這一層把代碼的中錨點位置獲取、剪切板信息複製、應用上下文、類中get、set的解析,以及最終把生成代碼織入到錨點後的操作。
  • infrastructure:在基礎層提供了工具類,用於獲取剪切板信息和錨點位置判斷等操作。

2. 織入代碼接口

cn.bugstack.guide.idea.plugin.application.IGenerateVo2Dto

```java public interface IGenerateVo2Dto {

void doGenerate(Project project, DataContext dataContext);

} ```

  • 定義接口其實非常重要的一步,因為這樣一步就把生成的標準定義下來了,所有的生成動作都要從這個接口發起。學習源碼也一樣,你要找到一個核心的入口點,才能更好的開始學習

3. 定義模板方法

因為生成代碼並織入錨點位置的操作,整個來看其實也是一套流程操作,因為在這個過程需要;獲取上下文信息(也就是工程對象)、給當前錨點位置的類提取 set 方法集合、之後在給Ctrl+C剪切板上的信息讀取出來提取 get 方法集合,第四步把set、get進行組合並織入代碼到錨點位置。整體過程如下:

  • 那麼在使用模板方法後,就可以非常容易的把寫在一個類裏的成片的代碼按照職責進行拆分。
  • 同時因為有了模板的定義,也就定義出了整個一套標準流程,在流程規範下執行代碼,後續再補充邏輯迭代功能也會更加容易。

4. 代碼織入錨點

關於代碼織入錨點前,我們在模板類中定義的方法,需要實現接口進行處理,重點包括: 1. 通過 CommonDataKeys.EDITOR.getData(dataContext)CommonDataKeys.PSI_ELEMENT.getData(dataContext) 封裝 GenerateContext 對象上下文信息,也就是一些類、錨點位置、文檔編輯的對象。 2. 通過 PsiClass 獲取光標位置對應的 Class 類信息,在通過 psiClass.getMethods() 讀取對象方法,把 set 方法過濾出來,封裝到集合中。 3. 通過 Toolkit.getDefaultToolkit().getSystemClipboard() 獲取剪切板信息,也就是你在錨點位置給對象生成 x.set(y.get) 時,複製的 Y y 對象,並開始提取 get 方法,同樣封裝到集合中。 4. 那麼最後就是代碼的組裝和織入動作了,這部分我們的代碼如下;

cn.bugstack.guide.idea.plugin.domain.service.impl.GenerateVo2DtoImpl

```java @Override protected void weavingSetGetCode(GenerateContext generateContext, SetObjConfigDO setObjConfigDO, GetObjConfigDO getObjConfigDO) { Application application = ApplicationManager.getApplication(); // 獲取空格位置長度 int distance = Utils.getWordStartOffset(generateContext.getEditorText(), generateContext.getOffset()) - generateContext.getStartOffset(); application.runWriteAction(() -> { StringBuilder blankSpace = new StringBuilder(); for (int i = 0; i < distance; i++) { blankSpace.append(" "); } int lineNumberCurrent = generateContext.getDocument().getLineNumber(generateContext.getOffset()) + 1; List setMtdList = setObjConfigDO.getParamList(); for (String param : setMtdList) { int lineStartOffset = generateContext.getDocument().getLineStartOffset(lineNumberCurrent++);

        WriteCommandAction.runWriteCommandAction(generateContext.getProject(), () -> {
            generateContext.getDocument().insertString(lineStartOffset, blankSpace + setObjConfigDO.getClazzParamName() + "." + setObjConfigDO.getParamMtdMap().get(param) + "(" + (null == getObjConfigDO.getParamMtdMap().get(param) ? "" : getObjConfigDO.getClazzParam() + "." + getObjConfigDO.getParamMtdMap().get(param) + "()") + ");\n");
            generateContext.getEditor().getCaretModel().moveToOffset(lineStartOffset + 2);
            generateContext.getEditor().getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
        });
    }
});

} ```

  • 織入代碼的流程動作,主要是對set方法集合進行遍歷,把對應的x.set(y.get)通過 document.insertString 到具體的位置和代碼。
  • 最終所有生成的代碼方法織入完成,即完成了整個 x.set(y.get) 的過程。

5. 配置菜單入口

plugin.xml

java <actions> <!-- Add your actions here --> <action id="Vo2DtoGenerateAction" class="cn.bugstack.guide.idea.plugin.action.Vo2DtoGenerateAction" text="Vo2Dto - 小傅哥" description="Vo2Dto generate util" icon="/icons/logo.png"> <add-to-group group-id="GenerateGroup" anchor="last"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift K"/> </action> </actions>

  • 這次我們給生成 x.set(y.get) 代碼的操作加個快捷鍵,可以讓我們更加方便的進行操作。

安裝使用驗證

接下來你就可以 So Easy 的轉換對象了,操作如下: 1. 複製你需要被轉換的對象,因為複製以後就可以被插件獲取到剪切板信息了,也就能提取到get方法集合。 2. 把鼠標定義到需要轉換設置值的對象,之後鼠標右鍵,選擇 Generate -> Vo2Dto - 小傅哥

1. 複製對象

2. 生成對象

3. 最終效果

  • 最終你就可以看到已經把你全部的對象轉換,自動生成出來代碼了,是不是很香。
  • 如果你直接使用快捷鍵 Ctrl + Shift + K 也是可以自動生成的。

拿去用用吧,最好再給提一些建議,提交issue、提交PR,都非常的歡迎!