解放雙手,自動生成“x.set(y.get)”,搞定vo2dto轉換

小傅哥發表於2021-12-15

作者:小傅哥
部落格:https://bugstack.cn

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

一、前言

給你機會,你也不中用啊

這些年從事程式設計開發以來,我好像發現了大部分研發那些不願意乾的事,都成就了別人。就像部署服務麻煩,有了Docker簡單CRUD不想開發,有了低程式碼給方法程式碼加監控繁瑣、有了非入侵的全鏈路監控

而這些原本你也在乾的事情,因為沒有想法、沒有創新、沒有思考,也可能是沒有能力,所以一直都是在搬磚、碼磚、砌磚,反反覆覆、來來回回。鍵盤敲的是越來越快了,程式碼搞的是越來越爛了。薪資沒搞上去,頭髮是越來越少了。

對於想走技術路線的碼農,千萬不要只是停留在業務功能的邏輯開發上,只有當你有了共性凝練的邏輯思維,才會逐步思考怎麼把一件重複的事做成一個通用的服務或者元件,而這些東西的落地不僅需要你會寫程式碼,還要會思考更要會去索引一些你需要的技術,並用自學的方式來補充這部分技能。

二、需求目的

你想寫物件間的get、set嗎?煩,煩死了,尤其是在DDD四層架構下,有了多層防汙處理,一會一個vo2dto、一會一個vo2do、一會一個do2po,雖然有很多工具的操作,但還是得寫呀。

怎麼辦?不要慌,這是機會呀,我們做個外掛搞定它,讓它可以自動的給我生成get、set程式碼,在IDEA Plugin的處理下,選擇好需要生成物件程式碼的錨點,複製下轉換物件,自動織入程式碼,1s鍾搞定!

三、案例開發

1. 工程結構

guide-idea-plugin-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

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

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

2. 織入程式碼介面

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

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

@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<String> setMtdList = setObjConfigDO.getParamList();
        for (String param : setMtdList) {
            int lineStartOffset = generateContext.getDocument().getLineStartOffset(lineNumberCurrent++);
            new WriteCommandAction(generateContext.getProject()) {
                @Override
                protected void run(@NotNull Result result) throws Throwable {
                    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);
                }
            }.execute();
        }
    });
}
  • 織入程式碼的流程動作,主要是對set方法集合進行遍歷,把對應的x.set(y.get)通過 document.insertString 到具體的位置和程式碼。
  • 最終所有生成的程式碼方法織入完成,即完成了整個 x.set(y.get) 的過程。

5. 配置選單入口

plugin.xml

<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) 程式碼的操作加個快捷鍵,可以讓我們更加方便的進行操作。

四、測試驗證

點選 Plugin 啟動 IDEA 外掛,之後有2步操作;

  1. 複製你需要被轉換的物件,因為複製以後就可以被外掛獲取到剪下板資訊了,也就能提取到get方法集合。
  2. 把滑鼠定義到需要轉換設定值的物件,之後滑鼠右鍵,選擇 Generate -> Vo2Dto - 小傅哥

1. 複製物件

2. 生成物件

3. 最終效果

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

五、擴充套件介面

獲取當前編輯的檔案, 通過PsiFile可獲得PsiClass, PsiField等 PsiFile psiFile = e.getData(LangDataKeys.PSI_FILE);
獲取當前的project物件 Project project = e.getProject();
獲取資料上下文 DataContext dataContext = e.getDataContext();
獲取到資料上下文後,通過CommonDataKeys物件可以獲得該File的所有資訊 Editor editor = CommonDataKeys.EDITOR.getData(dataContext);<br />PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(dataContext);<br />VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
GlobalSearchScope中有Project域,Moudule域,File域等等 PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, name, GlobalSearchScope);
類似於IDE中的Find Usages操作 Query<PsiReference> search = ReferencesSearch.search(PsiElement);
重新命名 RenameRefactoring newName = RefactoringFactory.getInstance(Project).createRename(PsiElement, "newName");
搜尋一個類的所有子類,過載方法較多,具體不再一一列出 Query<PsiClass> search = ClassInheritorsSearch.search(PsiClass);
根據類的全限定名查詢PsiClass,下面這個方法是查詢Project域 PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(classQualifiedName, GlobalSearchScope.projectScope(project));
獲取Java類所在的Package PsiPackage psiPackage = JavaPsiFacade.getInstance(Project).findPackage(classQualifiedName);
查詢被特定方法重寫的方法 Query<PsiMethod> search = OverridingMethodsSearch.search(PsiMethod);

六、總結

  • 本章節中我們涉及了不少對工程物件的類和方法進行操作的處理,這些內容的實踐也非常適合你在其他場景使用,比如給工程的介面生成一些自動化API的操作。
  • 在給物件生成 x.set(y.get) 的時候,我也在思考該怎麼更合理的把轉換物件代入到外掛的程式碼邏輯中,可能會想到是通過彈窗配置或者程式碼掃描到上一行,但這樣的方式終究是不舒服的,考慮到實際自己編碼的習慣操作,其實我們做這步的時候,複製是第一步動作,為了更好的體驗,所以這裡選擇了用複製來處理這塊的連線性問題。
  • 本系列的 IDEA Plugin 開發都以遵循 DDD 工程結構思想為設計和實現,雖然整體內容看上去也不復雜,但希望這些框架的沉澱可以為 DDD 落地鋪路,讓更多的工程研發人員適應 DDD 結構。

七、系列推薦

相關文章