一年前整理的 SpringBoot 專案規範怕丟了,畢竟當時用心寫了,現在補傳到部落格上。
1. 應用劃分規範
應用系統開發時,後端服務需要考慮按照模組/微服務的劃分原則來劃分出多個SpringBoot模組,以便未來微服務化的重構。應用劃分規範/原則如下:
- 橫向拆分:按照不同的業務域進行拆分,例如訂單、營銷、風控、積分資源等。形成獨立的業務領域微服務叢集。
- 縱向拆分:把一個業務功能裡的不同模組或者元件進行拆分。例如把公共元件拆分成獨立的原子服務,下沉到底層,形成相對獨立的原子服務層。這樣一縱一橫,就可以實現業務的服務化拆分。
要做好微服務的分層,需要梳理和抽取核心服務、公共應用,作為獨立的服務下沉到核心和公共能力層,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。服務拆分是越小越好嗎?微服務的大與小是相對的。比如在初期,我們把交易拆分為一個微服務,但是隨著業務量的增大,可能一個交易系統已經慢慢變得很大,並且併發流量也不小,為了支撐更多的交易量,會把交易系統,拆分為訂單服務、投標服務、轉讓服務等。因此服務的拆分力度需與具體業務相結合,總的原則是服務內部高內聚,服務之間低耦合。
2. 專案建立規範
一個應用系統中會根據情況,至少會建立一個SpringBoot專案及多個SpringBoot模組,建立工程時遵從以下規則:
GroupID
格式:com.{公司/BU }.業務線 [.子業務線]。說明:{公司/BU} 例如:alibaba/taobao/tmall/aliexpress 等 BU 一級;子業務線可選。
正例:com.taobao.jstorm 或 com.alibaba.dubbo.register
ArtifactID
格式:產品線名-模組名。語義不重複不遺漏,先到中央倉庫去查證一下。正例:dubbo-client / fastjson-api / jstorm-tool
Version
格式二方庫版本號命名方式:主版本號.次版本號.修訂號
1) 主版本號:產品方向改變,或者大規模 API 不相容,或者架構不相容升級。
2) 次版本號:保持相對相容性,增加主要功能特性,影響範圍極小的 API 不相容修改。
3) 修訂號:保持完全相容性,修復 BUG、新增次要功能特性等。
說明:注意起始版本號必須為:1.0.0,而不是 0.0.1。
2.1. 父模組建立規範
一個應用系統中會建立一個或多個SpringBoot專案,每個Spring Boot專案建立過程中(New -> Project
),遵循如下規範:
名稱 | 規範 |
---|---|
專案型別 | Spring Initializr |
Group | 遵循java包名規範,到公司層 |
Artifact | <專案/業務中心簡稱>-service 等 |
Type | Maven Project |
Language | Java |
Packaging | POM |
Java Version | 保持預設 |
Name | 專案名稱,與Artifact相同 |
Package | <Group>.<專案/業務中心簡稱> |
Version | 可選取當前時間較新的穩定版,例如release版本 |
其中 Version 版本號命名方式: 主版本號.次版本號.修訂號
- 主版本號:產品方向改變,或者大規模 API 不相容,或者架構不相容升級。
- 次版本號:保持相對相容性,增加主要功能特性,影響範圍極小的 API 不相容修改。
- 修訂號:保持完全相容性,修復 BUG、新增次要功能特性等。
注意:起始版本號必須為:1.0.0,而不是 0.0.1。
3. POM規範
3.1. 父模組POM規範
父模組(tl-service)POM檔案參考:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>com.df.tl</groupId>
<artifactId>tl-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tl-service</name>
<description>tl專案的父模組</description>
<!-- 屬性-->
<properties>
<java.version>1.8</java.version>
<spring-parent.version>2.4.5</spring-parent.version>
<lombok.version>1.18.0</lombok.version>
<mybatis.version>1.3.2</mybatis.version>
<mysql.version>8.0.16</mysql.version>
<druid.version>1.1.13</druid.version>
</properties>
<!-- 子模組,申明-->
<modules>
<module>tl-api</module>
<module>tl-project-service</module>
<module>tl-task-service</module>
</modules>
<!-- 打包方式 pom-->
<packaging>pom</packaging>
<!-- dependencyManagement 管理-->
<dependencyManagement>
<dependencies>
<!-- spring web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-parent.version}</version>
</dependency>
<!-- spring test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>${spring-parent.version}</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- druid 資料來源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<!-- pluginManagement 管理-->
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
需要注意的地方如下:
- properties:除了
java.version
以外,可以自定義屬性(非強制),如maven依賴的版本號。 - modules:需要申明所有子模組。
- packaging:父模組使用
pom
。該標籤有pom/jar/war
三種屬性,但父模組的功能通常只是打包,故使用 pom 即可。 - dependencyManagement:父模組建議使用
dependencyManagement
管理依賴,相較於dependencies
來說更靈活,不會導致子模組引入不必要的依賴。可自行查詢資料,對比dependencyManagement
和dependencies
的區別。 - pluginManagement:父模組建議使用
pluginManagement
管理外掛,相較於plugins
來說更靈活。可自行查詢資料,對比pluginManagement
和plugins
的區別。
3.2. 子模組POM規範
子模組(tl-project-service)POM檔案參考:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.df.tl</groupId>
<artifactId>tl-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.df.tl</groupId>
<artifactId>tl-project-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tl-project-service</name>
<description>專案管理模組</description>
<!-- 打包方式 jar-->
<packaging>jar</packaging>
<dependencies>
<!-- spring web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
需要注意的地方如下:
- parent:繼承父模組。
- packaging:子模組使用
jar
。 - dependencies:無論父模組用
dependencyManagement
還是dependencies
方式,子模組都一律用dependencies
。如果是前者,需要申明重複依賴,但可省略版本號;如果是後者,無需重複依賴。 - plugins:無論父模組用
pluginManagement
還是plugins
方式,子模組都一律用plugins
。如果是前者,需要申明重複依賴,但可省略版本號和配置的細節;如果是後者,無需重複依賴。
4. 分層規範
- 開放介面層:可直接封裝 Service 方法暴露成 RPC 介面;通過 Web 封裝成 http 介面;閘道器控制層等。
- 終端顯示層:各個端的模板渲染並執行顯示的層。當前主要是 velocity 渲染,JS 渲染,JSP 渲染,移動端展示等。
- Web 層:主要是對訪問控制進行轉發,各類基本引數校驗,或者不復用的業務簡單處理等。
- Service 層:相對具體的業務邏輯服務層。
Manager 層:通用業務處理層,它有如下特徵:
1) 對第三方平臺封裝的層,預處理返回結果及轉化異常資訊。2) 對 Service 層通用能力的下沉,如快取方案、中介軟體通用處理。
3) 與 DAO 層互動,對多個 DAO 的組合複用。
- DAO 層:資料訪問層,與底層 MySQL、Oracle、Hbase、OB 等進行資料互動。
- 外部介面或第三方平臺:包括其它部門 RPC 開放介面,基礎平臺,其它公司的 HTTP 介面。
這裡同樣提供單個專案工程的結構分層示例:
.
├── java
│ └── com
│ └── df
│ └── learn
│ └── exampleapi
│ ├── ExampleApiApplication.java
│ ├── config
│ │ └── ThreadPoolConfig.java
│ ├── constant
│ │ └── ThreadConstant.java
│ ├── controller
│ │ └── DemoController.java
│ ├── manager
│ │ └── UserManager.java
│ ├── mapper
│ │ └── HrUserMapper.java
│ ├── pojo
│ │ ├── bo
│ │ │ └── EmailInfoBO.java
│ │ ├── dto
│ │ │ └── UserInfoDTO.java
│ │ └── po
│ │ └── HrUserPO.java
│ ├── service
│ │ ├── DemoService.java
│ │ └── impl
│ │ └── DemoServiceImpl.java
│ └── util
│ └── EmailUtil.java
└── resources
├── application-dev.yml
├── application-prod.yml
├── application-uat.yml
├── application.yml
├── ehcache.xml
├── logback-spring.xml
├── mapper
│ └── HrUserMapper.xml
├── static
└── templates
現在解釋一下這些工程結構:
controller
:前端控制層 Controller。service
:資料服務介面層 Service。manager
:通用業務處理層 Manager。service.impl
:資料服務介面實現層 Service Implements。config
:配置類目錄。consts
:常量類目錄。util
:工具類目錄。mapper(dao)
:dao層目錄,如果是MyBatis專案可以用mapper。pojo
:包含PO/BO/VO/DTO等目錄。
POJO分層
- POJO(Plain Ordinary Java Object):是 DO/DTO/BO/VO 的統稱,POJO專指只有setter/getter/toString的簡單類,包括DO/DTO/BO/VO等,但禁止命名成 xxxPOJO。
- PO( Persistant Object):與資料庫表結構一一對應。也有使用 DO( Data Object)代替的。
- DTO( Data Transfer Object):資料傳輸物件,Service或Manager向外傳輸的物件,即也是Controller中,Request或Response所封裝的物件。
- BO( Business Object):業務物件。可以理解成Java開發過程中,抽象出來的一些與表結構無關的POJO,可能包含一到多個DO。
- VO( View Object):展示物件,它的作用是把某個指定頁面(或元件)的所有資料封裝起來。VO不常見,因為和DTO太相似,基本都用DTO替代。例如:男/女在資料庫中儲存為0/1,DTO中顯示的是0/1,VO中應該顯示的是男/女。但沒必要糾結,個人偏好直接用DTO。
5. 多環境配置規範
多環境下的YAML配置檔案,規範如下:
- application.yml -- 主配置檔案
- application-dev.yml -- 開發環境配置檔案
- application-uat.yml -- 測試環境配置檔案
- application-prod.yml -- 正式環境配置檔案
配置檔案示例如下:
spring:
profiles:
active: prod
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: root
一個專案上一般有好多套環境,開發環境,測試環境,UAT環境,生產環境,每個環境的引數不同,所以我們就需要把每個環境的引數配置到對應的yml檔案中,可以通過在主配置檔案中啟用當前的配置檔案,例如:
spring:
profiles:
active: prod
這行配置在application.yml 檔案中,意思是當前生效的配置檔案是application-prod.yml。
6. 異常處理規範
各層的異常處理要求
- DAO層:產生的異常型別有很多,無法用細粒度的異常進行 catch,使用 catch(Exception e)方式,並 throw new DAOException(e),不需要列印日誌,因為日誌在 Manager/Service 層一定需要捕獲並列印到日誌檔案中去,如果同臺伺服器再打日誌, 浪費效能和儲存。
- Service層:在 Service 層出現異常時,必須記錄出錯日誌到磁碟,儘可能帶上引數資訊, 相當於保護案發現場。
- Manager層:Manager 層與 Service 同機部署,日誌方式與 DAO 層處理一致,如果是單獨部署,則採用與 Service 一致的處理方式。
- Web層:Web 層絕不應該繼續往上拋異常,因為已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那麼就應該直接跳轉到友好錯誤頁面, 儘量加上友好的錯誤提示資訊。開放介面層要將異常處理成錯誤碼和錯誤資訊方式返回。
全域性異常處理思路
前文說到,Service
、Manager
和 Controller
層,是要有異常處理的。而這些需要處理的異常,往往可以劃分為幾類,不同的每類異常做對應的統一處理。例如:需要反饋給使用者的異常(如:賬號/密碼錯誤)、程式自行處理的異常(如:資料庫鎖衝突,樂觀鎖CAS的自動重試)、無非解決的伺服器異常(如:資料庫當機、網路波動等)... 。
針對這類可以統一分類處理的異常,建議編寫全域性異常處理的方法來實現,具體實現方法有下面三個步驟:
- 自定義異常類:使用統一的異常,也可以自定義異常類,如“需要反饋給使用者的異常”可以自定義業務異常類,手動丟擲提示語。
- 丟擲異常:因為有了統一處理,就需要標準化的丟擲異常。
- 全域性異常處理:對於異常的處理,總不能在每個方法處try catch,可以利用面向切面程式設計的思想做全域性異常處理。常見的全域性異常處理方式有:
@ControllerAdvice
、AOP
、過濾器
和攔截器
等。不過要注意它們的執行順序為(ServletContextListener> Filter > Interception > AOP > 具體執行的方法 > AOP > @ControllerAdvice > Interception > Filter > ServletContextListener
)。