8

一年前整理的 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)。

KerryWu
641 声望159 粉丝

保持饥饿