選定對(duì)象批量織入“x.set(y.get)”代碼,自動(dòng)生成vo2dto
本文轉(zhuǎn)載自微信公眾號(hào)「bugstack蟲(chóng)洞?!?,作者小傅哥。轉(zhuǎn)載本文請(qǐng)聯(lián)系bugstack蟲(chóng)洞棧公眾號(hào)。
一、前言
給你機(jī)會(huì),你也不中用啊
這些年從事編程開(kāi)發(fā)以來(lái),我好像發(fā)現(xiàn)了大部分研發(fā)那些不愿意干的事,都成就了別人。就像部署服務(wù)麻煩,有了Docker、簡(jiǎn)單CRUD不想開(kāi)發(fā),有了低代碼、給方法代碼加監(jiān)控繁瑣、有了非入侵的全鏈路監(jiān)控。
而這些原本你也在干的事情,因?yàn)闆](méi)有想法、沒(méi)有創(chuàng)新、沒(méi)有思考,也可能是沒(méi)有能力,所以一直都是在搬磚、碼磚、砌磚,反反復(fù)復(fù)、來(lái)來(lái)回回。鍵盤(pán)敲的是越來(lái)越快了,代碼搞的是越來(lái)越爛了。薪資沒(méi)搞上去,頭發(fā)是越來(lái)越少了。
對(duì)于想走技術(shù)路線的碼農(nóng),千萬(wàn)不要只是停留在業(yè)務(wù)功能的邏輯開(kāi)發(fā)上,只有當(dāng)你有了共性凝練的邏輯思維,才會(huì)逐步思考怎么把一件重復(fù)的事做成一個(gè)通用的服務(wù)或者組件,而這些東西的落地不僅需要你會(huì)寫(xiě)代碼,還要會(huì)思考更要會(huì)去索引一些你需要的技術(shù),并用自學(xué)的方式來(lái)補(bǔ)充這部分技能。
二、需求目的
你想寫(xiě)對(duì)象間的get、set嗎?煩,煩死了,尤其是在DDD四層架構(gòu)下,有了多層防污處理,一會(huì)一個(gè)vo2dto、一會(huì)一個(gè)vo2do、一會(huì)一個(gè)do2po,雖然有很多工具的操作,但還是得寫(xiě)呀。
怎么辦?不要慌,這是機(jī)會(huì)呀,我們做個(gè)插件搞定它,讓它可以自動(dòng)的給我生成get、set代碼,在IDEA Plugin的處理下,選擇好需要生成對(duì)象代碼的錨點(diǎn),復(fù)制下轉(zhuǎn)換對(duì)象,自動(dòng)織入代碼,1s鐘搞定!效果視頻:
三、案例開(kāi)發(fā)
1. 工程結(jié)構(gòu)
- 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塊區(qū)域:
action:提供菜單欄窗體,在插件中我們把這個(gè)菜單欄配置到 Generate 下,也就是通常你生成 get、set、constructor 方法的地方。
application:應(yīng)用層定義接口,這里定義了一個(gè)用于生成代碼并織入到錨點(diǎn)的方法接口。
domian:領(lǐng)域?qū)訉iT(mén)處理代碼的生成和織入動(dòng)作,這一層把代碼的中錨點(diǎn)位置獲取、剪切板信息復(fù)制、應(yīng)用上下文、類(lèi)中g(shù)et、set的解析,以及最終把生成代碼織入到錨點(diǎn)后的操作。
infrastructure:在基礎(chǔ)層提供了工具類(lèi),用于獲取剪切板信息和錨點(diǎn)位置判斷等操作。
2. 織入代碼接口
cn.bugstack.guide.idea.plugin.application.IGenerateVo2Dto
- public interface IGenerateVo2Dto {
- void doGenerate(Project project, DataContext dataContext);
- }
定義接口其實(shí)非常重要的一步,因?yàn)檫@樣一步就把生成的標(biāo)準(zhǔn)定義下來(lái)了,所有的生成動(dòng)作都要從這個(gè)接口發(fā)起。學(xué)習(xí)源碼也一樣,你要找到一個(gè)核心的入口點(diǎn),才能更好的開(kāi)始學(xué)習(xí)
3. 定義模板方法
因?yàn)樯纱a并織入錨點(diǎn)位置的操作,整個(gè)來(lái)看其實(shí)也是一套流程操作,因?yàn)樵谶@個(gè)過(guò)程需要;獲取上下文信息(也就是工程對(duì)象)、給當(dāng)前錨點(diǎn)位置的類(lèi)提取 set 方法集合、之后在給Ctrl+C剪切板上的信息讀取出來(lái)提取 get 方法集合,第四步把set、get進(jìn)行組合并織入代碼到錨點(diǎn)位置。整體過(guò)程如下:
那么在使用模板方法后,就可以非常容易的把寫(xiě)在一個(gè)類(lèi)里的成片的代碼按照職責(zé)進(jìn)行拆分。
同時(shí)因?yàn)橛辛四0宓亩x,也就定義出了整個(gè)一套標(biāo)準(zhǔn)流程,在流程規(guī)范下執(zhí)行代碼,后續(xù)再補(bǔ)充邏輯迭代功能也會(huì)更加容易。
4. 代碼織入錨點(diǎn)
關(guān)于代碼織入錨點(diǎn)前,我們?cè)谀0孱?lèi)中定義的方法,需要實(shí)現(xiàn)接口進(jìn)行處理,重點(diǎn)包括:
- 通過(guò) CommonDataKeys.EDITOR.getData(dataContext)、CommonDataKeys.PSI_ELEMENT.getData(dataContext) 封裝 GenerateContext 對(duì)象上下文信息,也就是一些類(lèi)、錨點(diǎn)位置、文檔編輯的對(duì)象。
- 通過(guò) PsiClass 獲取光標(biāo)位置對(duì)應(yīng)的 Class 類(lèi)信息,在通過(guò) psiClass.getMethods() 讀取對(duì)象方法,把 set 方法過(guò)濾出來(lái),封裝到集合中。
- 通過(guò) Toolkit.getDefaultToolkit().getSystemClipboard() 獲取剪切板信息,也就是你在錨點(diǎn)位置給對(duì)象生成 x.set(y.get) 時(shí),復(fù)制的 Y y 對(duì)象,并開(kāi)始提取 get 方法,同樣封裝到集合中。
- 那么最后就是代碼的組裝和織入動(dòng)作了,這部分我們的代碼如下;
cn.bugstack.guide.idea.plugin.domain.service.impl.GenerateVo2DtoImpl
- @Override
- protected void weavingSetGetCode(GenerateContext generateContext, SetObjConfigDO setObjConfigDO, GetObjConfigDO getObjConfigDO) {
- Application application = ApplicationManager.getApplication();
- // 獲取空格位置長(zhǎng)度
- 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();
- }
- });
- }
- 織入代碼的流程動(dòng)作,主要是對(duì)set方法集合進(jìn)行遍歷,把對(duì)應(yīng)的x.set(y.get)通過(guò) document.insertString 到具體的位置和代碼。
- 最終所有生成的代碼方法織入完成,即完成了整個(gè) x.set(y.get) 的過(guò)程。
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) 代碼的操作加個(gè)快捷鍵,可以讓我們更加方便的進(jìn)行操作。
四、測(cè)試驗(yàn)證
點(diǎn)擊 Plugin 啟動(dòng) IDEA 插件,之后有2步操作;
- 復(fù)制你需要被轉(zhuǎn)換的對(duì)象,因?yàn)閺?fù)制以后就可以被插件獲取到剪切板信息了,也就能提取到get方法集合。
- 把鼠標(biāo)定義到需要轉(zhuǎn)換設(shè)置值的對(duì)象,之后鼠標(biāo)右鍵,選擇 Generate -> Vo2Dto - 小傅哥
1. 復(fù)制對(duì)象
2. 生成對(duì)象
3. 最終效果
最終你就可以看到已經(jīng)把你全部的對(duì)象轉(zhuǎn)換,自動(dòng)生成出來(lái)代碼了,是不是很香。
如果你直接使用快捷鍵 Ctrl + Shift + K 也是可以自動(dòng)生成的。
五、擴(kuò)展接口
獲取當(dāng)前編輯的文件, 通過(guò)PsiFile可獲得PsiClass, PsiField等 | PsiFile psiFile = e.getData(LangDataKeys.PSI_FILE); |
獲取當(dāng)前的project對(duì)象 | Project project = e.getProject(); |
獲取數(shù)據(jù)上下文 | DataContext dataContext = e.getDataContext(); |
獲取到數(shù)據(jù)上下文后,通過(guò)CommonDataKeys對(duì)象可以獲得該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域,F(xiàn)ile域等等 | PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, name, GlobalSearchScope); |
類(lèi)似于IDE中的Find Usages操作 | Query<PsiReference> search = ReferencesSearch.search(PsiElement); |
重命名 | RenameRefactoring newName = RefactoringFactory.getInstance(Project).createRename(PsiElement, "newName"); |
搜索一個(gè)類(lèi)的所有子類(lèi),重載方法較多,具體不再一一列出 | Query<PsiClass> search = ClassInheritorsSearch.search(PsiClass); |
根據(jù)類(lèi)的全限定名查詢PsiClass,下面這個(gè)方法是查詢Project域 | PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(classQualifiedName, GlobalSearchScope.projectScope(project)); |
獲取Java類(lèi)所在的Package | PsiPackage psiPackage = JavaPsiFacade.getInstance(Project).findPackage(classQualifiedName); |
查找被特定方法重寫(xiě)的方法 | Query<PsiMethod> search = OverridingMethodsSearch.search(PsiMethod); |
六、總結(jié)
本章節(jié)中我們涉及了不少對(duì)工程對(duì)象的類(lèi)和方法進(jìn)行操作的處理,這些內(nèi)容的實(shí)踐也非常適合你在其他場(chǎng)景使用,比如給工程的接口生成一些自動(dòng)化API的操作。
在給對(duì)象生成 x.set(y.get) 的時(shí)候,我也在思考該怎么更合理的把轉(zhuǎn)換對(duì)象代入到插件的代碼邏輯中,可能會(huì)想到是通過(guò)彈窗配置或者代碼掃描到上一行,但這樣的方式終究是不舒服的,考慮到實(shí)際自己編碼的習(xí)慣操作,其實(shí)我們做這步的時(shí)候,復(fù)制是第一步動(dòng)作,為了更好的體驗(yàn),所以這里選擇了用復(fù)制來(lái)處理這塊的連接性問(wèn)題。
本系列的 IDEA Plugin 開(kāi)發(fā)都以遵循 DDD 工程結(jié)構(gòu)思想為設(shè)計(jì)和實(shí)現(xiàn),雖然整體內(nèi)容看上去也不復(fù)雜,但希望這些框架的沉淀可以為 DDD 落地鋪路,讓更多的工程研發(fā)人員適應(yīng) DDD 結(jié)構(gòu)。