鸡翅Club-01
鸡翅Club的学习笔记都是我学习鸡哥的项目时编写的,感兴趣可以去支持一下鸡哥:程序员鸡翅
本篇要点
- DDD架构的开发模式搭建
- EasyCode代码生成器的使用
- MapStruct对象映射技术
- 利用Druid的能力对数据库密码加密
- 使用guava进行参数校验
- 日志打印
DDD架构的开发模式搭建
传统的三层架构
传统的三层架构能够快速开发简单系统,主要分为三层
- 表现层(Controller):主要负责处理HTTP请求,参数校验等功能
- 业务逻辑层(Service):主要时核心的业务逻辑,通常与数据库表结构强耦合
- 数据访问层(Mapper/Dao):直接操作数据库

DDD架构模式
DDD架构是复杂的业务模型,着重解决领域问题,传统的DDD架构主要分为四层:
- 用户接口层(Interface):类似于Controller,但是仅仅负责协议的装换,不包含任何的业务逻辑
- 应用层(Application):协调领域对象完成用例,在这一层可以进行事务的控制、权限校验等
- 领域层(Domain):核心是实体、值对象(VO)、领域服务,主要的业务规则就在此
- 基础设施层(Infrastructure):主要用于提供技术实现,如数据库访问、消息队列等

两种架构模式的对比
| 对比维度 | 传统三层架构 | DDD架构 |
|---|---|---|
| 设计目标 | 技术分层,快速开发简单系统 | 复杂业务建模,解决领域问题 |
| 核心分层 | 表现层、业务逻辑层、数据访问层 | 用户接口层、应用层、领域层、基础设施层 |
| 关注点 | 技术实现(如何做) | 业务语义(做什么,为什么) |
| 领域逻辑位置 | 集中在业务逻辑层(Service) | 集中在领域层(Domain) |
| 代码组织方式 | 按技术职责分层(如Controller/Mapper) | 按业务能力划分(聚合根、实体、值对象) |
| 数据库驱动 | 数据库表结构驱动设计 | 领域模型驱动设计(数据库仅为持久化) |
鸡翅Club项目中的DDD架构
在鸡翅Club项目中的DDD架构进行了微服务的拓展,结合了传统的DDD分层和微服务特有的扩展层(如启动层、聚合层)

API层
第一天的学习暂时未涉及到
- 定位:对外接口层,统一暴露对外部的接口
Application层
- 定位:应用层,主要是协调领域层完成具体的用例,也就是具体的请求(如创建订单),权限处理等
- 关键组件
- controller:接收API请求,调用领域服务,但是此处的controller是应用层的入口,并非MVC中的Controller
- service:主要是组合多个领域对象的能力
- mq/job:应该是消息队列和定时任务相关,第一天暂未设计到
- 数据载体:应用层的数据载体主要是DTO,接收请求参数以及装换为BO调用领域层服务
Domain层
- 定位:领域层,核心业务的的归属地
- 关键组件:
- service:领域服务,处理跨聚合根的逻辑。聚合根用于定义业务一致性的边界,并管理聚合内的实体和值对象,例如在项目中的刷题模块,题目是主实体,作为刷题模块的聚合根,题目分类、标签等都是附属的信息。
- 数据载体:领域层的数据载体主要是BO,但是调用基础设置层时需要转换成PO,也就是与数据库表字段对应的实体
Infra层
- 定位:基础设施层,为其他层提供技术实现,如数据库查询
- 关键组件:
- basic:主要提供数据库访问的方法,以及存放数据库实体PO
- 这里应该还有很多内容,但是第一天都暂未设计
- 数据载体:PO作为输入输出
Starter层
- 定位:启动层。非业务逻辑的入口,聚合能力的模块,如SpringBoot的 @SpringApplication 启动类就是放在这里的,同时这里也进行一些数据源的配置(yaml文件)
Common层
- 定位:公共层。主要是一些公用的部分,如枚举类、常量类等。其他模块需要使用时直接在pom文件中引入即可
数据载体与转换关系
| 层 | 输入数据 | 输出数据 | 转换逻辑 |
|---|---|---|---|
| Application层 | DTO | DTO | DTO ↔ BO(调用领域层时转换) |
| Domain层 | BO | BO | BO ↔ PO(持久化时转换) |
| Infra层 | PO | PO | PO ↔ 外部模型(如RPC返回的DTO) |
这种DDD架构模式下的业务开发逻辑
以新增题目简单业务为例。在应用层收到客户端发送的请求,得到请求体后,将接收请求体的DTO转换为领域层需要的实体BO,接着调用领域层服务;在领域层将接收到的BO装换为基础设施层需要的PO,然后调用基础设施层的服务进行数据持久化。注意,这只是一个简单的新增业务,如果是负责业务,可能在每一层都有其他的处理。

EasyCode代码生成器的使用
EasyCode是一个基于数据库表生成相关代码的插件,和MyBatisPlus代码生成插件类似,都是可以随便点点就可以生成代码的实用工具。
插件的安装
idea安装插件可能会因为网络问题失败,因此我也写过一篇文章->如何手动下载插件并导入IDEA

如何进行代码生成
在idea中进行数据库的连接后,可以右键相关表选择EasyCode进行代码生成

在生成代码的选项窗口中可以选择生成代码的位置和生成哪些内容

MapStruct对象映射技术
在接触鸡哥的项目前,我自己在开发中也遇到过不同实体类之间的装换,但是我都是通过结合HuTool工具包手动进行实体间的转换,如下:
Articles articles = new Articles();
BeanUtil.copyProperties(articleDTO, articles);在了解了对象映射技术MapStruct后觉得这种方式确实麻烦了一点。MapStruct是一个用于Java的代码生成器,它通过注解处理器在编译时自动生成安全的 Bean映射代码,可以显著减少手写样板代码。
实现MapStruct进行不同实体之间的转换
导入正确的包
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</dependency>不同实体的转换逻辑一般放在同级包 convert 下,下面是一个示例:
@Mapper // 注意不要导错包
public interface SubjectCategoryDTOConverter {
// 静态方法获取 Mapper 实例
SubjectCategoryDTOConverter INSTANCE = Mappers.getMapper(SubjectCategoryDTOConverter.class);
/**
* DTO 转 BO
* @param subjectCategoryDTO
* @return
*/
SubjectCategoryBO convertToCategoryBO(SubjectCategoryDTO subjectCategoryDTO);
/**
* BO 转 DTO
* @param subjectCategoryBO
* @return
*/
SubjectCategoryDTO convertToCategoryDTO(SubjectCategoryBO subjectCategoryBO);
/**
* BO 列表 转 DTO 列表
* @param subjectCategoryBOList
* @return
*/
List<SubjectCategoryDTO> convertToCategoryDTOList(List<SubjectCategoryBO> subjectCategoryBOList);
}如何使用
可以在需要进行实体转换的地方直接按照如下的方式进行转换
SubjectCategoryBO bo = SubjectCategoryDTOConverter.INSTANCE
.convertToCategoryBO(dto);利用Druid的能力对数据库密码加密
在Druid中存在一个工具类 ConfigTools,其中有一个方法:genKeyPair() 可以获取到一对公钥和私钥
String[] keyPair = ConfigTools.genKeyPair(512);
privateKey = keyPair[0]; // 私钥
System.out.println("privateKey:" + privateKey);
publicKey = keyPair[1]; // 公钥
System.out.println("publicKey:" + publicKey);将数据库密码通过私钥以及方法:ConfigTools.encrypt() 加密后作为配置文件中的数据库密码
String encrypt = ConfigTools.encrypt(privateKey, plainText);
System.out.println("encrypt:" + encrypt); // 使用私钥加密后的密码再对Druid进行如下的配置,就可以成功加密数据库密码了(其中publicKey为之前获得的公钥):
druid:
connectionProperties: config.decrypt=true;config.decrypt.key=${publicKey}; # 数据库密码加密场景使用 guava 进行参数校验
Guava(Google Core Libraries for Java)是 Google 提供的一个 Java 工具库,包含许多实用的工具类,其中 Preconditions 是专门用于参数校验的核心组件。它提供了一组静态方法,用于在方法或构造函数的开头快速验证参数的有效性,并在校验失败时抛出标准异常(如 IllegalArgumentException、NullPointerException 等)
Guava Preconditions 的核心方法
| 方法 | 说明 | 异常类型 |
|---|---|---|
checkArgument(boolean) | 检查参数是否符合条件(如 age > 0) | IllegalArgumentException |
checkNotNull(T) | 检查参数是否为 null | NullPointerException |
checkState(boolean) | 检查对象状态是否合法(如初始化状态) | IllegalStateException |
checkElementIndex(int, int) | 检查数组/列表索引是否有效 | IndexOutOfBoundsException |
checkPositionIndex(int, int) | 检查字符串/数组位置是否有效 | IndexOutOfBoundsException |
使用示例
// 校验 age 必须 > 0
Preconditions.checkArgument(age > 0, "年龄必须大于 0,当前值: %s", age);
// 校验 name 和 email 不能为 null
Preconditions.checkNotNull(name, "用户名不能为 null");
Preconditions.checkNotNull(email, "邮箱不能为 null");
// 检查索引是否在有效范围内 [0, list.size())
Preconditions.checkElementIndex(index, list.size(), "索引越界");Guava 相较与我之前写项目的纯 if + throw 参数校验,确实方便了不少,而且代码也变得更加简洁。
日志打印
在项目中我们可以在不同地方进行相应的日志打印来确定BUG出现在哪个地方,如:
log.info("新增分类,DTO 参数:{}", JSON.toJSONString(dto));由于我们是采用JSON序列化打印的,在打印这行日志之前会先执行序列化。但是在微服务高并发的场景下,即使这条日志不打印也会执行序列化,是非常好性能的操作,因此我们可以采用下列的方式打印,以防止这种情况:
if (log.isInfoEnabled()) {
/*
先判断的原因主要是微服务高并发场景下,在打印之前会先进行序列化,
导致即使这个日志不打印也会执行一遍序列化
所以先判断日志级别,如果为info级别再打印,可以避免这种情况
*/
log.info("新增分类,DTO 参数:{}", JSON.toJSONString(dto));
}