如何搭建一個超級好用的JavaWeb框架?

2019-10-13     工控自動化專家

之前在Web開發框架推導一文中我們一步步的搭建了一個開發框架。

在當時的情況下,還算滿足需求。但是隨著項目的逐漸完善,需求變更的頻度逐漸變得比新增需求的頻度高,原來框架的弊端越來越明顯,所以需要對框架進行升級改進。

我們先來看原來框架的問題,然後基於這些問題,來對框架進行改進。

一、原框架的問題:

1.代碼生成問題

2.參數傳遞問題

3.Service層問題

4.測試依賴問題

5.Mapper.xml的問題

代碼生成問題

在原框架中,我們基於各種約束,編寫了一個代碼生成組件,通過這個組件,我們可以針對選中的表來生成Controller,Service,Model,Mapper等一系列的類,也就是說,只要建完表,就可以直接生成一套CRUD,直接就可以啟動並測試。這在項目初期看起來很美,但是在需求變動時,還是有很多的局限性。

首先,生成的代碼邏輯是固化的。如果稍微有些調整,就需要調整生成代碼的組件,然後重新打包,上傳到jar倉庫,項目修改組件版本,再進行代碼生成,整個流程過於繁瑣。

其次,為了方便代碼的生成,其實是做了不少妥協的:

1.為了方便在修改表欄位以後,能夠重新生成,很多類都抽象了一個基類用於操作Model欄位。這些基類不能夠手動修改,因為每次生成都會覆蓋。這實際導致了類的數量的增多。

2.生成的CRUD固化了,不能手動調整。如果生成的CRUD不滿足需求,不能直接在代碼上修改。只能拷貝一份進行修改,因為再次生成時會覆蓋。這導致了代碼的冗餘。

3.Param和Result委託了Model,這在Model發生改變時,能在編譯期就能知道對應欄位的調整。但是也引入了不少問題,我們在「參數傳遞問題」一節單獨討論。

參數傳遞問題

當初為了便於代碼的生成,決定Param和Result都繼承Model,這導致了如下的一些問題:

1.使得Param和Result都依賴了Model。但是Param和Result是視圖層模型,而Model是持久層模型,兩者的進化度並不是一致的。但是現在的繼承關係導致了在默認情況下視圖層模型的進化需要和持久層同步,當然你也可以手動調整Param和Result,但是這又導致了代碼生成的優勢沒有了。

2.Param和Result通過委託的方式來設置欄位,也就是說,它們實際是沒有欄位的,通過getter和setter將值設置到了Model中。這就沒法使用lombok來簡化getter和setter,使得Param和Result代碼行數較多

3.同時,對於swagger來說,有些註解需要基於欄位,導致某些功能無法實現(例如:ModelAttribute),只能基於額外手段來處理(例如:需要通過ApiImplicitParams來實現欄位文檔)。

4.CRUD都是基於同一個Param和Result,導致前端的接口會顯示很多無用的欄位,加大前端理解接口的難度

Service層問題

Service層有如下問題:

1.Service層的職責過重,包括了事務處理、參數設置、業務邏輯

2.導致Service中的代碼是麵條代碼,不利於業務邏輯的理解

3.同時事務註解是直接加在類上的,Spring的默認事務機制會導致類似如下代碼的邏輯調用不會拋出期望的異常

// PostService
public String savePost(Post post) {
postRepository.save(post);
for(PostDiscuss discuss : post.getDiscuss()) {
// 這裡是抓不到RuntimeException異常的,會是一個TransactionRollBack的異常
discussService.save(discuss);
}
}
// discussService
public String savePost(PostDiscuss discuss) {
throw new RuntimeException("保存失敗");
}

測試依賴問題

核心的業務邏輯在Service中,測試還是需要依賴於Spring,當項目越來越大時,啟動項目的時間越來越長,可能要1分鐘甚至更長。這就導致單元測試效率越來越低。

Mapper.xml的問題

在面試的時候,我經常會問下面的一些問題:

1.Java裡面接口的作用是什麼?

2.Service、DAO為什麼要編寫接口,再去實現這個接口?

3.接口和實現在相同的模塊下,反正都要重新打包的。多寫個接口不是多寫了好幾行代碼嗎?

4.和上面類似的問題,Mybatis裡面,聲稱將sql獨立到了Mapper.xml文件中,使得可以不需要編譯直接修改sql。但Mapper.xml都是和Class放在一起的,改了還是需要重新打包,而且Mybatis是不能動態加載Mapper.xml的,那把sql獨立到XML里,到底有什麼優勢?

對於最後一個問題,我的答案是,對於大部分項目來說,沒什麼優勢!項目易不易於部署、擴展,不在於你使用的框架,而在於你的設計。

就以Mapper.xml來說,Mybatis將sql與代碼分離了,但是你在項目里還是將Mapper.xml和代碼放在同一個模塊下,那這個優勢就沒有了。既然沒有這個優勢,我們還有必要單獨寫Mapper.xml文件嗎?我的選擇是,那就不寫了,直接使用Mybatis提供的註解。

同時為了解決Service層對DAO層(這裡也就是對Mybatis)的強依賴,對框架進行了一些改進,解耦Service和DAO層。具體見下面的改進方案。

二、框架改進方案

為了解決上面這些問題,對框架進行了如下調整:

1.分離Param、Result和Model

2.替換代碼生成

3.獨立業務邏輯

4.Model層優化

分離Param、Result和Model

上面已經提到了Param、Result和Model強耦合會有很多問題,所以這裡就將Param、Result和Model分離開。每個都是獨立的Bean,這就解決了上面幾個問題。但是引入了兩個新問題:

1.首先,很明顯的,增加了手動編碼的量。當一個表修改了欄位,需要修改三個類甚至更多的類

2.其次,增加了數據傳遞之間的代碼。即Param傳遞到Model,需要對欄位賦值。如果一個欄位一個欄位的設值,會增加很多無聊的代碼。而使用反射的話會對性能有一些影響

那如何解決這兩個問題呢?首先,純手擼肯定是不可能的。需要提供一些自動化手段。

對於賦值來說,Spring提供了BeanUtils來簡化處理,雖然是基於反射來設值的,但是對於現階段來說,這點性能損耗還是沒什麼影響的。但是,BeanUtils對於不同類型的屬性不能進行拷貝,假設我有一個Domain對象Book,裡面有個欄位Author,現在我要賦值給BookResult,其中有個欄位AuthorResult,此時BeanUtils是無法賦值的。所以我編寫了一個基於Gson的工具類來處理,性能測試10000次的屬性拷貝BeanUtils需要500多毫秒,基於Gson的工具類只需要300毫秒左右。

對於表欄位的生成,如果使用的是IDEA的話,IDE默認提供了一個腳本,可以從表來生成POJO!我們可以使用這個腳本來生成Model,然後將欄位拷貝到Param和Result中,來簡化欄位的編寫。我對這個腳本進行了修改,以符合項目需求。主要增加了lombok的支持,新增了類注釋和欄位注釋。

替換代碼生成

對於上面代碼生成組件的問題,我調整了代碼生成的方式。不再基於組件來生成,而是基於IDEA本身的FileTemplate、LiveTemplate以及Scripted Extensions來進行生成。雖然這樣的方式,不能夠一次性生成多個文件,但是由於生成邏輯基本是一次性的,所以影響不是很大。在初次生成代碼時,代碼生成組件的效率是高於FileTemplate、LiveTemplate以及Scripted Extensions的組合,但是後期調整的靈活性,明顯是FileTemplate、LiveTemplate以及Scripted Extensions的組合要高於代碼生成組件的:

1.首先,當文件結構調整時,只需要修改FileTemplate,並將配置文件導出給項目組成員即可。

2.同樣的,當LiveTemplate調整時,也只需要修改對應的LiveTemplate,並將配置文件導出給項目組成員即可。

3.其次,想生成哪個文件,只要針對這個文件生成即可

4.第三,通過FileTemplate生成完整的文件後,可以通過LiveTemplate快速的進行模塊化的編碼

5.最後,FileTemplate可以設置為項目級別,即每個項目可以有獨立的FileTemplate

具體的操作流程,在下面演示。

獨立業務邏輯

針對Service和測試的問題,將原來的Controller、Service和Model三層,拆分為四層:

1.Controller負責前端數據的接收和返回,以及統一異常處理

2.Service負責事務以及Domain層邏輯的組裝。這裡就不會出現事務嵌套問題,也就不會導致抓不到期望的異常的問題

3.Domain負責業務邏輯

4.Model負責數據持久

這樣Service的職責減輕了,同時不再有事務嵌套的問題。

Model層優化

上面提到,框架中最終放棄了Mapper.xml,轉而使用Mybatis的註解來實現持久化操作。改用註解,規避了XML代碼的編寫,但是並沒有解決框架對Mybatis的強依賴。所以這裡在Domain中新增了Repository接口層,此層用於定義Domain的持久化操作,而Model層中對Repository進行實現,這裡的實現就是Mybatis實現。這樣做有兩個好處:

1.依賴倒置:原來是Domain依賴Model層,而現在是Model層依賴Domain層,這樣當我要把Mybatis替換掉時,Domain完全無感知。

2.獨立測試:因為現在Domain不依賴於其它任何層,所以可以脫離資料庫和容器來進行測試。使得測試的效率不會隨著項目的開發而越來越低


三、框架改進細節

現在已經知道了,如何對框架進行改進,我們現在就開始著手進行改造。其實主要的改造是對代碼生成方式的改造,也就是編寫FileTemplate、LiveTemplate和ScriptedExtensions。下面對這三個功能進行簡單的說明,先說ScriptedExtensions。

Scripted Extensions

先來解釋一下,什麼是Scripted Extensions。我們都知道,現在的IDE都是插件式的,也就是說,我們可以通過開發商提供的插件開發包來開發插件,擴展現有的IDE功能。但是編寫插件需要特定的開發環境,如果是一個很簡單的功能,還要費勁去搭開發環境,挺麻煩的。所以IDEA提供了Scripted Extensions,可以理解為一個簡化版的插件,就是可以通過腳本來擴展IDE功能。

IDEA提供了Database功能,可以連接資料庫進行相關操作。當你連接了資料庫,在表上右擊時,可以看到Scripted Extensions這個選項,裡面有一個功能是可以基於表來生成POJO的groovy腳本。

但是功能比較low:

1.包名是寫死的:com.sample

2.沒有生成table注釋

3.沒有基於lombok來簡化getter和setter

不過好在,我們能基於這個腳本來自行修改,在剛才的Scripted Extensions菜單里,有個Go to Scripts Directory選項,點擊後,可以進入腳本目錄。

直接對這個groovy文件Ctrl+c,Ctrl-v,複製一份,重命名一下,基於這個腳本進行修改即可。具體怎麼修改,按照自己的需求來,裡面主要就是根據表信息對String的拼接而已。

FileTemplate

FileTemplate是IDEA提供的生成文件的模板,你在點擊菜單的File->New...以後,出現的各種文件,都是基於FileTemplate來實現的。我們自定義的Controller、Service、Domain等類,都可以通過FileTemplate來簡化創建。

具體使用方式為,按下Ctrl-Alt-S呼出設置菜單,點擊Editor->File And Code Template,在裡面新增Template即可。

幾點說明:

1.下面的描述中列出了默認的一些參數以及作用

2.你也可以自定義變量,自定義的變量如果沒有賦值,在創建時會有輸入框提示輸入內容

3.模板是基於Velocity的,所以如果你熟悉 Velocity,那就可以直接上手

4.Enable Live Template選項是在FileTemplate激活LiveTemplate變量,不過需要使用#[[]]#包裹。但是對於創建Java,這個功能有bug,並不能定位到需要的位置,所以暫時沒使用

創建完成後,就可以在New菜單中看到這個模板了。

LiveTemplate

LiveTemplate實際就是CodeSnippet。創建方式和FileTemplate類似。按下Ctrl-Alt-S呼出設置菜單,點擊Editor->Live Template,在裡面新增Template即可。

幾點說明:

1.這裡的變量是使用$$包裹

2.每個變量就是一個占位符,在使用tab展開後,可以手動輸入值

3.右下角的Edit variables,用於對變量賦值,IDEA提供了一些方法、也可以設置默認值

4.下面的change連結,可以選擇LiveTemplate生效的位置,比如只在Java類聲明處生效

編碼流程

創建了上面的幾個模板後,編碼流程如下:

1.在表上右擊,通過Scripted Extensions來生成Model

2.通過FileTemplate來快速生成Controller、Service、Domain等類

3.通過LiveTemplate來快速編寫代碼

四、總結

本文通過對原框架問題的梳理及解決,來對框架進行升級改造,以適應項目的發展和推進。

文章來源: https://twgreatdaily.com/B36-w20BMH2_cNUgQWW6.html