Contents

鸡翅Club-01

鸡翅Club的学习笔记都是我学习鸡哥的项目时编写的,感兴趣可以去支持一下鸡哥:程序员鸡翅

本篇要点

  • DDD架构的开发模式搭建
  • EasyCode代码生成器的使用
  • MapStruct对象映射技术
  • 利用Druid的能力对数据库密码加密
  • 使用guava进行参数校验
  • 日志打印

DDD架构的开发模式搭建

传统的三层架构

传统的三层架构能够快速开发简单系统,主要分为三层

  • 表现层(Controller):主要负责处理HTTP请求,参数校验等功能
  • 业务逻辑层(Service):主要时核心的业务逻辑,通常与数据库表结构强耦合
  • 数据访问层(Mapper/Dao):直接操作数据库

https://aetherial.cn/oss/aetherial/2025/05/0188542a-7a0f-4760-97b9-72d28a5847ee_image.png

DDD架构模式

DDD架构是复杂的业务模型,着重解决领域问题,传统的DDD架构主要分为四层:

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

https://aetherial.cn/oss/aetherial/2025/05/54ab2043-bcc5-4572-a40a-b48a4352b077_image.png

两种架构模式的对比

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

鸡翅Club项目中的DDD架构

在鸡翅Club项目中的DDD架构进行了微服务的拓展,结合了传统的DDD分层和微服务特有的扩展层(如启动层、聚合层)

https://aetherial.cn/oss/aetherial/2025/05/7ff7519d-9e05-48b3-bddc-1a20e74f1297_image.png

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层DTODTODTO ↔ BO(调用领域层时转换)
Domain层BOBOBO ↔ PO(持久化时转换)
Infra层POPOPO ↔ 外部模型(如RPC返回的DTO)

这种DDD架构模式下的业务开发逻辑

以新增题目简单业务为例。在应用层收到客户端发送的请求,得到请求体后,将接收请求体的DTO转换为领域层需要的实体BO,接着调用领域层服务;在领域层将接收到的BO装换为基础设施层需要的PO,然后调用基础设施层的服务进行数据持久化。注意,这只是一个简单的新增业务,如果是负责业务,可能在每一层都有其他的处理。

https://aetherial.cn/oss/aetherial/2025/05/03f4c243-accc-4c8b-a3e5-5aae0fbcf66a_image.png

EasyCode代码生成器的使用

EasyCode是一个基于数据库表生成相关代码的插件,和MyBatisPlus代码生成插件类似,都是可以随便点点就可以生成代码的实用工具。

插件的安装

idea安装插件可能会因为网络问题失败,因此我也写过一篇文章->如何手动下载插件并导入IDEA

https://aetherial.cn/oss/aetherial/2025/05/ad1ee0d1-4b47-4ded-87e0-34eb5af70173_image.png

如何进行代码生成

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

https://aetherial.cn/oss/aetherial/2025/05/f744b699-2b47-4b74-ba19-49a290b450cb_image.png

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

https://aetherial.cn/oss/aetherial/2025/05/c8242b02-a1f9-44e0-8a6f-35942698460a_image.png

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 > 0IllegalArgumentException
checkNotNull(T)检查参数是否为 nullNullPointerException
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));
}