解放雙手,自動生成「x.set(y.get)」,搞定vo2dto轉換
作者:小傅哥
博客://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
源碼獲取:#公眾號:bugstack蟲洞棧
回復:idea
即可下載全部 IDEA 插件開發源碼
在此 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. 代碼織入錨點
關於代碼織入錨點前,我們在模板類中定義的方法,需要實現接口進行處理,重點包括:
- 通過
CommonDataKeys.EDITOR.getData(dataContext)
、CommonDataKeys.PSI_ELEMENT.getData(dataContext)
封裝 GenerateContext 對象上下文信息,也就是一些類、錨點位置、文檔編輯的對象。 - 通過 PsiClass 獲取光標位置對應的 Class 類信息,在通過
psiClass.getMethods()
讀取對象方法,把 set 方法過濾出來,封裝到集合中。 - 通過
Toolkit.getDefaultToolkit().getSystemClipboard()
獲取剪切板信息,也就是你在錨點位置給對象生成x.set(y.get)
時,複製的 Y y 對象,並開始提取 get 方法,同樣封裝到集合中。 - 那麼最後就是代碼的組裝和織入動作了,這部分我們的代碼如下;
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步
操作;
- 複製你需要被轉換的對象,因為複製以後就可以被插件獲取到剪切板信息了,也就能提取到get方法集合。
- 把鼠標定義到需要轉換設置值的對象,之後鼠標右鍵,選擇
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 結構。
七、系列推薦
- 方案設計:基於IDEA插件開發和位元組碼插樁技術,實現研發交付質量自動分析
- 剛火了的中台轉頭就拆,一大波公司放不下又拿不起來!
- 《IntelliJ IDEA 插件開發》第4節:擴展創建工程嚮導步驟,開發DDD腳手架
- HashMap核心知識,擾動函數、負載因子、擴容鏈表拆分,深度學習
- p3c 插件,是怎麼檢查出你那屎山的代碼?