選定對象批量織入“x.set(y.get)”代碼,自動生成vo2dto
本文轉載自微信公眾號「bugstack蟲洞棧」,作者小傅哥。轉載本文請聯系bugstack蟲洞棧公眾號。
一、前言
給你機會,你也不中用啊
這些年從事編程開發以來,我好像發現了大部分研發那些不愿意干的事,都成就了別人。就像部署服務麻煩,有了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. 代碼織入錨點
關于代碼織入錨點前,我們在模板類中定義的方法,需要實現接口進行處理,重點包括:
- 通過 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 結構。