add by zhj: 正在開發Java項目,Java比Python是復雜的多,比如結構化的參數要定義為bean,所以有DTO, BO, VO,很繁瑣。
如果項目是前后端分離的,建議只用DTO和BO就可以了
原文:http://www.iocoder.cn/Onemall/Application-layer/ 「芋道源碼」
1. 概述
本文,我們來分享下 Onemall 電商開源項目的后端的應用分層規范。
目前,Onemall 的電商后端,采用 Spring Boot + Dubbo 的方式,提供給前端接口,也就是說,采用前后端分離的方式。為什么要提這一點呢,我們往下來瞅瞅。
2. 阿里巴巴規范
在說 Onemall 的應用分層規范之前,我們先來看看阿里巴巴分享的應用分層規范。
如下內容,引用自 《阿里巴巴Java開發手冊(詳盡版)》 。
考慮到排版,下面內容就不使用引用先。
- 【推薦】圖中默認上層依賴於下層,箭頭關系表示可直接依賴,如:開放接口層可以依賴於Web層,也可以直接依賴於Service層,依此類推:
- 開放接口層:可直接封裝Service方法暴露成RPC接口;通過Web封裝成http接口;進行網關安全控制、流量控制等。
- 終端顯示層:各個端的模板渲染並執行顯示的層。當前主要是velocity渲染,JS渲染,JSP渲染,移動端展示等。
- Web層:主要是對訪問控制進行轉發,各類基本參數校驗,或者不復用的業務簡單處理等。
- Service層:相對具體的業務邏輯服務層。
- Manager層:通用業務處理層,它有如下特征: - 1) 對第三方平台封裝的層,預處理返回結果及轉化異常信息; - 2) 對Service層通用能力的下沉,如緩存方案、中間件通用處理; - 3) 與DAO層交互,對多個DAO的組合復用。
- DAO層:數據訪問層,與底層MySQL、Oracle、Hbase等進行數據交互。
- 外部接口或第三方平台:包括其它部門RPC開放接口,基礎平台,其它公司的HTTP接口。
-
【參考】(分層異常處理規約)在DAO層,產生的異常類型有很多,無法用細粒度的異常進行catch,使用catch(Exception e)方式,並throw new DAOException(e),不需要打印日志,因為日志在Manager/Service層一定需要捕獲並打印到日志文件中去,如果同台服務器再打日志,浪費性能和存儲。在Service層出現異常時,必須記錄出錯日志到磁盤,盡可能帶上參數信息,相當於保護案發現場。如果Manager層與Service同機部署,日志方式與DAO層處理一致,如果是單獨部署,則采用與Service一致的處理方式。Web層絕不應該繼續往上拋異常,因為已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那么就應該跳轉到友好錯誤頁面,加上用戶容易理解的錯誤提示信息。開放接口層要將異常處理成錯誤碼和錯誤信息方式返回。
-
【參考】分層領域模型規約:
- DO(Data Object):與數據庫表結構一一對應,通過DAO層向上傳輸數據源對象。
- DTO(Data Transfer Object):數據傳輸對象,Service或Manager向外傳輸的對象。
- BO(Business Object):業務對象。由Service層輸出的封裝業務邏輯的對象。
- AO(Application Object):應用對象。在Web層與Service層之間抽象的復用對象模型,極為貼近展示層,復用度不高。
- VO(View Object):顯示層對象,通常是Web向模板渲染引擎層傳輸的對象。
- Query:數據查詢對象,各層接收上層的查詢請求。注意超過2個參數的查詢封裝,禁止使用Map類來傳輸。
3. Onemall 的選擇
看到阿里巴巴的規范之后,胖友是不是一臉懵逼,竟然有這么多 POJO ?!每個公司的業務復雜度不同,架構不同,所以 POJO 的選擇實際會有不同。當然,艿艿覺得,原則上是 Service 不將 DO 數據庫實體從 Service 暴露到 Controller ,避免后續數據庫設計的變化,影響暴露出去的方法。
🔥 如下圖,是 Onemall 的應用分層選擇:
我們按照自下而上,來看看各層的選擇。
- 按照 Controller、Service、DAO 分成三層,去掉 Manager 層。
- 大多數業務場景下,無需與第三方平台對接。
- 當然,如果需要和第三方對接,還是會封裝成 Client ,例如說 Pay Client 和 第三方支付平台的對接。😈 所以實際還是有“隱藏”的 Manager 層。
- DAO 層
- 入參,使用 DO(Data Object)。
- 出參,使用 DO(Data Object)。
- Service 層
- 入參,使用 DTO(Data Transfer Object)。
- 需要加上 Bean Validation 注解,從而校驗參數。
- 需要加上 Swagger API 注解,因為后續 Controller 很大可能性會使用到它,從而生成 API 文檔。更細的原因,我們在 Controller 層一起講。
- 示例:AdminAddDTO 和 AdminUpdateDTO 。
- 出參,使用 BO(Business Object)。
- 入參,使用 DTO(Data Transfer Object)。
- Controller 層
- 入參,使用 DTO(Data Transfer Object)。
- 因為前后端分離之后,Controller 大多數情況下,基本是將 Service 進行封裝,提供 API 接口。所以大多數情況,Service DTO 可以重用,所以就默許使用 Service DTO 。😈 當然,這塊有不同意見的胖友,可以一起來討論下,我也挺糾結的。
- 當然,如果 Service DTO 不夠用的情況下,可以自己在創建下 Controller DTO 。
- 本來想 Controller 單獨在取個 XXO 的名字,結果想了半天沒想出來,就繼續沿用 Service DTO 了。
- 所以,因為是這樣的設定,我們就要求 Service DTO 上,增加 Swagger API 注解。
- 出參,使用 BO(Business Object)。
- 原因,也是同 Controller 入參。
- 當然,如果 Service BO 不夠用的情況下,可以自己在創建下 Controller VO 。
- 入參,使用 DTO(Data Transfer Object)。
艿艿:每個公司的分層架構不同,歡迎一起討論。妥妥的。
因為分層規范是后來調整的,所以項目中可能有部分不符合這樣的規范,具體以示例為主。
🔥 聊完了應用分層的話題,我們在一起討論下 Service 邏輯異常的時候,如何進行返回。這里的邏輯異常,我們指的是,例如說用戶名已經存在,商品庫存不足等。一般來說,常用的方案選擇,有兩種:
- 封裝統一的業務異常類 ServiceException ,里面有錯誤碼和錯誤提示,然后進行
throws
拋出。 - 封裝通用的返回類 CommonResult ,里面有錯誤碼和錯誤提示,然后進行
return
返回。
一開始,我們選擇了 CommonResult ,結果發現如下情況:
- 因為 Spring
@Transactional
聲明式事務,是基於異常進行回滾的,如果使用 CommonResult 返回,則事務回滾會非常麻煩。 - 當調用別的方法時,如果別人返回的是 CommonResult 對象,還需要不斷的進行判斷,寫起來挺麻煩的。
所以,后來我們采用了拋出業務異常 ServiceException 的方式。
🔥 可能機智的小伙伴會問,如果拋出異常,Controller 如何做通用的處理,答案在 GlobalExceptionHandler 類。結果 Spring MVC 的 Exception 處理機制,我們會將 ServiceException 轉換成 CommonResult 對象,返回給前端。
當然,故事還沒有結束,Controller 雖然返回的是 BO / VO 對象,我們選擇在外面包了一層 CommonResult ,用於返回可能存在的業務邏輯錯誤的情況。因為呢,HTTP API 是語言無關,無法使用 Java Excpetion 。
不過哈,最初我們使用了 @ControllerAdvice
機制,自動全局將 BO / VO 對象,包裝成 CommonResult 對象,但是和基友 didi 討論了下這個選擇,建議還是 Controller 顯示聲明 CommonResult 返回,考慮點是 AOP 不應該破壞方法的 Schema ,即有一天去掉這個 AOP ,依然返回的是 CommonResult 。
4. 彩蛋
🔥 最后的最后,大家總是會討論到的一個問題,這么多 POJO 對象,如何進行復制呢?Onemall 采用 mapstruct ,因為廣告法的原因,我們不能說它是最好用的,但是的確是(並且,效果還非常非常非常的高),哈哈哈哈。具體的示例,可以看看 AdminConvert 。