SpringBoot 專案規範筆記.2021

KerryWu發表於2022-02-07

一年前整理的 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 等
TypeMaven Project
LanguageJava
PackagingPOM
Java Version保持預設
Name專案名稱,與Artifact相同
Package<Group>.<專案/業務中心簡稱>
Version可選取當前時間較新的穩定版,例如release版本
其中 Version 版本號命名方式: 主版本號.次版本號.修訂號
  1. 主版本號:產品方向改變,或者大規模 API 不相容,或者架構不相容升級。
  2. 次版本號:保持相對相容性,增加主要功能特性,影響範圍極小的 API 不相容修改。
  3. 修訂號:保持完全相容性,修復 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 來說更靈活,不會導致子模組引入不必要的依賴。可自行查詢資料,對比 dependencyManagementdependencies 的區別。
  • pluginManagement:父模組建議使用 pluginManagement 管理外掛,相較於 plugins 來說更靈活。可自行查詢資料,對比 pluginManagementplugins 的區別。

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 層絕不應該繼續往上拋異常,因為已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那麼就應該直接跳轉到友好錯誤頁面, 儘量加上友好的錯誤提示資訊。開放介面層要將異常處理成錯誤碼和錯誤資訊方式返回。
全域性異常處理思路

前文說到,ServiceManagerController 層,是要有異常處理的。而這些需要處理的異常,往往可以劃分為幾類,不同的每類異常做對應的統一處理。例如:需要反饋給使用者的異常(如:賬號/密碼錯誤)、程式自行處理的異常(如:資料庫鎖衝突,樂觀鎖CAS的自動重試)、無非解決的伺服器異常(如:資料庫當機、網路波動等)... 。

針對這類可以統一分類處理的異常,建議編寫全域性異常處理的方法來實現,具體實現方法有下面三個步驟:

  1. 自定義異常類:使用統一的異常,也可以自定義異常類,如“需要反饋給使用者的異常”可以自定義業務異常類,手動丟擲提示語。
  2. 丟擲異常:因為有了統一處理,就需要標準化的丟擲異常。
  3. 全域性異常處理:對於異常的處理,總不能在每個方法處try catch,可以利用面向切面程式設計的思想做全域性異常處理。常見的全域性異常處理方式有:@ControllerAdviceAOP過濾器攔截器等。不過要注意它們的執行順序為(ServletContextListener> Filter > Interception > AOP > 具體執行的方法 > AOP > @ControllerAdvice > Interception > Filter > ServletContextListener)。

相關文章