基础微服务项目架构构建总结
1. Maven 依赖版本管理
1.1 Maven 依赖的优先级
1、最短路径优先
- 工程中依赖了 B、C 两个 jar 包
- 在 B jar 包内引用了 C jar 包版本为 1.0
- 在工程内直接引用的 C jar 包版本为 2.0
Project -> B -> C(1.0) ,Project -> C(2.0)。由于 C(2.0) 路径最短,所以项目使用的是 C(2.0)
2、POM 申明顺序优先
如果 project -> B -> C(1.0) ,project -> D -> C(2.0) 这样的路径长度一样怎么办呢?
这样的情况下,Maven 会根据 POM 文件声明的顺序加载,如果先声明了 B,后声明了 D,那就最后的依赖就会是 C(1.0)
1.2 Maven 包版本控制
在项目顶层的父 POM 中可以定义如下的三方 jar 的版本定义(这里的 jar 包只是定义,并没有引用,即不会下载依赖)
<dependencyManagement>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.43</version>
</dependency>
</dependencyManagement>
这样需要引用这个 jar 的子模块可以忽略版本定义直接引用(此时才真正下载依赖)
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
1.3 多项目全局管理
随便打开一个 SpringBoot 的项目,打开 POM 文件,父级依赖都是
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${springboot-version}</version>
</parent>
点击这个 parent 的依赖进去看到顶级的父级依赖为
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot-version}</version>
</parent>
继续点进去,里面已经没有任何 jar 的实际引用了,只有各种各样的 SpringBoot 或者 Spring 生态可能会依赖到的 jar 包的版本定义
Spring 通过定义一个顶层的父级版本依赖,只要是符合 SpringBoot 大版本下的 Spring 组件内的各个 jar 版本都是统一的,如果出现依赖升级的情况,不需要再去升级一个个组件的版本,直接升级父级的依赖管理 POM 中的版本即可
参照 Spring Maven 版本管理的思路,我们也可以定义这样一个业务的顶层 Maven 版本管理工程,如 common-dependency
- 版本管理工程的 POM 的父 POM 依赖 spring-boot
- 版本管理工程的 POM 内定义业务通用的一些 Maven 依赖版本
- 推送该工程至中央仓库(本地可以直接执行
maven install
打包到本地仓库) - 业务应用将父 POM 从 spring-boot 切换为 common-dependency
即在单个业务项目上抽离出一个版本管理工程作为父工程,所有的项目都使用统一的通用的依赖版本,假如某个项目需要自定义依赖或依赖版本,在项目的顶层 POM 文件中再进行定义即可,根据依赖优先级,会优先使用项目的 POM 文件中自定义的依赖版本
1.4 common-dependency
1、新建一个项目
2、删掉多余的其他文件
3、进行版本管理,父依赖为 spring-boot-starter-parent
注:这里 build
里的 spring-boot-maven-plugin 插件只是为了演示同样有依赖版本继承所以才在此处定义(注释状态),该插件只需定义在项目的主启动类的 POM 文件里即可。单模块项目不影响,假如为多模块项目,打包时会报错 Unable to find main class
<?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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
</parent>
<groupId>fan</groupId>
<artifactId>common-dependency</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modules>
<module>demo</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<name>common-dependency</name>
<description>common-dependency</description>
<!-- 统一管理jar包版本 -->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<lombok.version>1.18.24</lombok.version>
<mybatis.plus.version>3.5.1</mybatis.plus.version>
<druid.version>1.2.9</druid.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- Mybatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!--
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
-->
</project>
4、推送到中央仓库,本地的话 maven install 到本地仓库
5、其他项目父依赖改为 common-dependency,即可进行统一的依赖版本管理
<?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">
<parent>
<groupId>fan</groupId>
<artifactId>common-dependency</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>fan</groupId>
<artifactId>common-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<name>common-project</name>
<description>common-project</description>
<!-- 统一通用的依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
依赖结构如下,不同的颜色的框表示不同的层级,或者说独立工程。如 common-dependency 为通用的顶层版本依赖工程,与 project 是独立的。project 将父依赖设置为 common-dependency,其内部模块的父依赖即相应的项目顶层 POM,module 是包含在 project 里的
单项目的话可以直接在项目顶层 POM 中进行版本控制即可,即从上图的 project 开始,或者说直接将 common-dependency 当成 project,这时 common-dependency 应该改为对应的项目名
2. 项目内划分模块
2.1 分模块
业务模块划分没有一个严格的业界标准,也没有说一定要按照怎么设计,这里根据个人使用总结为以下几个模块,具体使用可根据情况自己进行调整:
Maven 模块 | 模块描述 | 特殊说明 |
---|---|---|
api | 将 rpc 相关的接口、所必须的交互实体、枚举等定义在此处,提供给内部其他系统进行服务调用 | 单体服务可去除此模块 |
base/comm | 与业务无关的通用配置定义在此处,如统一结果返回类、统一工具类等。具有业务无关性,与业务相关的工具类、枚举等可定义在具体的业务模块内 | |
rpc | api 包的 rpc 接口定义实现,一般来说是调用模块内的具体业务接口进行相关的处理 | 单体服务可去除此模块 |
service | 具体的服务模块,进行业务处理,不特指某一个模块名 | |
web | 在此处定义启动类,配置文件(resources 目录),配置类(RedisConfig/MyBatisConfig)等项目配置 |
依赖结构如下,在之前的基础上添加项目内部划分的模块间的依赖关系
此处同样可以将上述划分好模块的 project 抽离出来成一个 common-project 用于多项目的统一的通用配置
2.2 common-project
1、同样的新建一个项目
2、删除其他多余文件,并按照前面的模块划分创建好对应的模块
3、顶层 POM 如下,父依赖为 common-dependency,并把各个模块依赖进去
<?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">
<parent>
<groupId>fan</groupId>
<artifactId>common-dependency</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modules>
<module>common-api</module>
<module>common-base</module>
<module>common-rpc</module>
<module>common-service</module>
<module>common-web</module>
</modules>
<groupId>fan</groupId>
<artifactId>common-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<name>common-project</name>
<description>common-project</description>
<!-- 统一通用的依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<!-- 将各模块依赖进来 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fan</groupId>
<artifactId>common-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fan</groupId>
<artifactId>common-rpc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fan</groupId>
<artifactId>common-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fan</groupId>
<artifactId>common-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4、同时根据依赖关系依赖对应的模块
如 common-api 依赖 common-base
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common-project</artifactId>
<groupId>fan</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-api</artifactId>
<name>common-api</name>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-base</artifactId>
</dependency>
</dependencies>
</project>
common-rpc 又依赖 common-api。由于 common-api 已经依赖了 common-base,所以不需要重复引入 common-base
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common-project</artifactId>
<groupId>fan</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-rpc</artifactId>
<name>common-rpc</name>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-api</artifactId>
</dependency>
</dependencies>
</project>
common-service 依赖 common-base
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common-project</artifactId>
<groupId>fan</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-service</artifactId>
<name>common-service</name>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-base</artifactId>
</dependency>
</dependencies>
</project>
同样推送到中央仓库,本地的话 maven install 到本地仓库
3. 业务模块内分层
3.1 分层
目前很多业务系统都是基于 MVC 三层架构来开发的,MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层。MVC 三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会 100% 遵从 MVC 固定的分层方式,而是会根据具体的项目需求,做适当的调整
很多 Web 或者 App 项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,一般就将后端项目分为 Repository 层、Service 层、Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。这里的 Service 层假如业务复杂,可再细分为如下三层:
- Manager 层: 负责将 Dao 层中的数据库操作组合复用,主要是一些缓存方案,中间件的处理,以及对第三方平台的封装
- Service 层: 更加关注业务逻辑,是业务处理层,将 Manager 组合过的操作和业务逻辑组合在一起,再封装成业务操作
- Biz 层: 包含 Service 层,Service 层注重基础业务的处理,Biz 层是复杂应用层的业务层
当然,这只是其中一种分层和命名方式。不同的项目、不同的团队,可能会对此有所调整。不过,万变不离其宗,只要是依赖数据库开发的 Web 项目,基本的分层思路都大差不差
3.2 数据载体划分
3.2.1 PO(Persistant Object)/Entity
持久化对象,通过 DAO 层向上传输的数据源对象,实体属性与表字段一一对应。简单来说 PO 就是数据库中的记录,一个 PO 的数据结构对应着库中表的结构,表中的一条记录就是一个 PO 对象,通常 PO 里面除了 getter,setter 之外没有别的方法。概念与 Entity 一致
3.2.2 BO(Business Object)
业务对象,由 Service 层输出的封装业务逻辑的对象。BO 即 PO 的组合,如 PO1 是交易记录,PO2 是商品浏览记录,PO3 是添加购物车记录,等等组合起来形成 BO ,就是个人网站行为对象
一类业务就会对应一个 BO,数量上没有限制,而且 BO 会有很多业务操作,也就是说除了 getter,setter 方法以外,BO 会有很多针对自身数据进行计算的方法
现在很多持久层框架自身就提供了数据组合的功能,因此 BO 有可能是在业务层由业务来拼装 PO 而成,也有可能是在数据库访问层由框架直接生成
3.2.3 DO
DO 主要有两种定义
- 一种在阿里巴巴开发手册中的定义,DO( Data Object),等同于上面的 PO
- 一种是在 DDD(Domain-Driven Design)领域驱动设计中,DO(Domain Object),等同于上面的 BO
3.2.4 DTO(Data Transfer Object)
数据传输对象,这个传输通常指的前后端之间的传输,Service 或 Manager 向外传输的对象
BO 和 DTO 的区别
这两个的区别主要是就是字段的删减。BO 对内,为了进行业务计算需要辅助数据,或者是一个业务有多个对外的接口,BO 可能会含有很多接口对外所不需要的数据,而 DTO 在 BO 的基础上,只要自己需要的数据,然后对外提供。在这个关系上,通常不会有数据内容的变化
现在微服务盛行,服务和服务之间调用的传输对象能叫 DTO 吗?
DTO 本身的一个隐含的意义是要能够完整的表达一个业务模块的输出,如果服务和服务之间相对独立,那就可以叫 DTO;如果服务和服务之间不独立,每个都不是一个完整的业务模块,拆开可能仅仅是因为计算复杂度或者性能的问题,那这就不能够叫做 DTO,只能是 BO
3.2.5 VO(Value Object)
数据展示对象,通常是 Web 向模板渲染引擎层传输的对象,字段值与前端要求的字段名称保持一致。即 JSON 里的数据对象
VO 和 DTO 的区别
对于绝大部分的应用场景来说,DTO 和 VO 的属性值基本是一致的,而且它们通常都是 POJO,因此没必要多此一举,但这是实现层面的思维,对于设计层面来说,概念上还是应该存在 VO 和 DTO,因为两者有着本质的区别,DTO 代表服务层需要接收的数据和返回的数据,而 VO 代表展示层需要显示的数据
通常可能的区别如下:
- 字段不一样,假如这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,VO 可能会根据需要删减一些字段
- 值不一样,VO 会根据需要对 DTO 中的值进行展示业务的解释
比如服务层有一个 getUser()
的方法返回一个系统用户,其中有一个属性是 gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定
- DTO 可能是这样的:
{"gender": "男", "age": 35}
- 经过业务解释的 VO 是这样的:
{"gender":"帅哥", "age": "30~39"}
这时可能说,在服务层直接就返回“帅哥美女”不就行了吗?
对于大部分应用来说,这不是问题,但如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从单一职责原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的 DTO,不应该出现与表现形式的耦合
3.2.6 Query
数据查询对象,各层接收上层的查询请求,超过 2 个参数的查询封装,禁止使用 Map 类来传输
3.3 结构图
示例结构图如下,个人理解可能不一样,可根据具体情况进行调整
4. 项目实践
4.1 结构图
这里由于是单体服务,去掉了 api 和 rpc 模块,假如有多个服务需要互相调用的话,加上 api 和 rpc 模块同样依赖对应的 common-api 和 common-rpc 模块即可,同时服务模块都依赖 common-service
4.2 实践
1、按照上面的模块划分,创建好项目
2、依赖 common-project 对应的模块
如 resource_nav_comm 依赖 common-base
<?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">
<parent>
<groupId>fan</groupId>
<artifactId>ResourceNavigation</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>fan</groupId>
<artifactId>resource_nav_comm</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<name>resource_nav_comm</name>
<description>resource_nav_comm</description>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-base</artifactId>
</dependency>
</dependencies>
</project>
resource_nav_web 依赖 common-web
<?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">
<parent>
<groupId>fan</groupId>
<artifactId>ResourceNavigation</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>resource_nav_web</artifactId>
<name>resource_nav_web</name>
<dependencies>
<dependency>
<groupId>fan</groupId>
<artifactId>common-web</artifactId>
</dependency>
</dependencies>
</project>
resource_nav_system 模块依赖 common-service
3、然后再根据上面的模块内分层,在对应层进行相应的开发