Spring Boot三层架构实战:从Controller-Service-Dao到DDD分层设计 📅 发布时间:2026/9/2 2:16:53 👁 浏览次数: 最近在开发一个后台管理系统时遇到了一个头疼的问题前端页面样式混乱后端接口逻辑臃肿数据库查询效率低下。每次修改一个功能都像在拆一个缠在一起的毛线球牵一发而动全身。为了解决这个问题我开始深入研究并实践软件架构中的分层设计。经过几个项目的迭代我深刻体会到一个清晰、合理的分层架构不仅能极大提升代码的可读性和可维护性更能让团队协作变得顺畅让系统在面对需求变更时从容不迫。本文将以一个典型的 Web 应用为例从零开始手把手带你搭建一个具备“美好分层感”的项目。我们将从最基础的三层架构Controller-Service-Dao讲起逐步深入到领域驱动设计DDD中的分层思想并结合 Spring Boot 框架提供完整的、可运行的代码示例。无论你是刚入门的 Java 开发者还是希望优化现有项目结构的资深工程师都能从本文中找到实用的思路和落地方案。1. 什么是分层架构为什么我们需要它在深入代码之前我们必须先理解分层架构的核心思想。简单来说分层架构就是将软件系统按照不同的职责划分为多个层次每一层只专注于完成特定的任务并且只与相邻的层进行交互。1.1 分层架构解决了什么问题想象一下如果你把所有的代码处理 HTTP 请求、验证数据、执行业务逻辑、访问数据库、组装返回结果都写在一个巨大的类或方法里会发生什么难以阅读和维护代码像一团乱麻逻辑纠缠不清。难以测试一个功能依赖于整个系统的状态无法进行独立的单元测试。难以复用业务逻辑和数据库访问代码粘在一起无法在其他地方复用。难以协作多个开发者同时修改同一块代码冲突频发。难以替换技术如果想更换数据库如从 MySQL 换到 PostgreSQL你需要修改大量散落在各处的 SQL 语句。分层架构正是为了解决这些痛点而生。它通过分离关注点让每一层各司其职。1.2 经典的三层架构对于大多数 Web 应用最经典和实用的就是三层架构表现层 (Presentation Layer / Controller Layer)负责接收用户的请求如 HTTP 请求进行简单的参数校验然后调用服务层处理最后将处理结果封装成响应如 JSON返回给用户。它不包含任何业务逻辑。业务逻辑层 (Business Logic Layer / Service Layer)这是系统的核心。它包含了所有的业务规则、流程和逻辑。它接收来自表现层的参数组织业务操作并调用数据访问层来持久化或获取数据。一个 Service 方法应该代表一个完整的业务用例。数据访问层 (Data Access Layer / Dao Layer)负责与数据库、缓存、消息队列等持久化设施进行交互。它封装了所有数据操作的细节为上层的业务逻辑层提供统一的数据访问接口。业务逻辑层不需要关心数据是来自 MySQL 还是 Redis。这种分层带来了清晰的数据流用户请求 - Controller - Service - Dao - 数据库响应则反向流动。每一层的变化都被限制在本层内极大地降低了系统的耦合度。2. 环境准备与项目初始化在开始编码前我们需要准备好开发环境。本文将以 Spring Boot 2.7.x 和 Java 11 为例进行演示。2.1 环境要求JDK: 11 或以上版本Maven: 3.6 或以上版本IDE: IntelliJ IDEA 或 Eclipse (推荐 IDEA)数据库: MySQL 5.7 或以上版本 (本文示例使用 MySQL)2.2 创建 Spring Boot 项目使用 Spring Initializr 快速创建一个项目选择以下依赖Spring Web: 用于构建 Web 层ControllerSpring Data JPA: 用于简化数据访问层操作MySQL Driver: MySQL 数据库驱动Lombok: 简化 Java Bean 的编写可选但强烈推荐生成的pom.xml关键依赖部分如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 请使用稳定版本 -- relativePath/ /parent groupIdcom.example/groupId artifactIdlayered-architecture-demo/artifactId version0.0.1-SNAPSHOT/version namelayered-architecture-demo/name descriptionDemo project for Layered Architecture/description properties java.version11/java.version /properties dependencies !-- Web 层 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 数据访问层 (JPA) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project2.3 配置数据库连接在src/main/resources/application.properties文件中配置数据库连接信息# 应用端口 server.port8080 # 数据库配置 spring.datasource.urljdbc:mysql://localhost:3306/layered_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyour_password # 请替换为你的数据库密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver # JPA 配置 spring.jpa.database-platformorg.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.hibernate.ddl-autoupdate # 开发环境可用 update生产环境建议使用 validate 或 none spring.jpa.show-sqltrue # 开发时显示 SQL便于调试 spring.jpa.properties.hibernate.format_sqltrue # 格式化 SQL 输出请确保你已经创建了名为layered_demo的数据库。3. 从零构建一个分层清晰的项目我们将以一个简单的“用户管理”功能为例实现用户的增删改查CRUD并严格按照三层架构来组织代码。3.1 项目目录结构规划一个清晰的分层首先体现在目录结构上。建议采用按功能模块分包再按层分子包的方式。src/main/java/com/example/layereddemo/ ├── LayeredArchitectureDemoApplication.java # 启动类 └── module/ └── user/ # 用户模块 ├── controller/ # 表现层 │ └── UserController.java ├── service/ # 业务逻辑层 │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java ├── dao/ # 数据访问层 (也可命名为 repository) │ └── UserDao.java ├── entity/ # 实体层 (与数据库表映射) │ └── User.java └── dto/ # 数据传输对象层 (用于层间传输) ├── UserDTO.java └── UserQueryDTO.java这种结构的好处是当你要找用户相关的代码时直接进入user包所有层次的代码一目了然高内聚、低耦合。3.2 实体层 (Entity Layer)实体类代表业务领域中的核心概念并与数据库表映射。它属于数据访问层的基础。// 文件路径src/main/java/com/example/layereddemo/module/user/entity/User.java package com.example.layereddemo.module.user.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; Data Entity Table(name sys_user) // 指定表名 public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) // 主键自增 private Long id; Column(nullable false, unique true, length 50) // 非空唯一长度限制 private String username; Column(nullable false, length 100) private String password; Column(nullable false, length 50) private String nickname; Column(nullable false, unique true, length 100) private String email; Column(name create_time, updatable false) // 数据库字段名不可更新 private LocalDateTime createTime; Column(name update_time) private LocalDateTime updateTime; // 在持久化前自动设置时间 PrePersist protected void onCreate() { createTime LocalDateTime.now(); updateTime LocalDateTime.now(); } PreUpdate protected void onUpdate() { updateTime LocalDateTime.now(); } }说明这里使用了 JPA 注解和 Lombok 的Data注解。实体类应专注于定义数据结构和与数据库的映射关系不应包含业务逻辑。3.3 数据访问层 (DAO Layer)DAO 层负责所有数据库操作。使用 Spring Data JPA我们可以通过接口声明的方式极大简化代码。// 文件路径src/main/java/com/example/layereddemo/module/user/dao/UserDao.java package com.example.layereddemo.module.user.dao; import com.example.layereddemo.module.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import java.util.Optional; Repository // 标识为数据访问组件 public interface UserDao extends JpaRepositoryUser, Long, JpaSpecificationExecutorUser { // Spring Data JPA 会根据方法名自动生成查询 OptionalUser findByUsername(String username); OptionalUser findByEmail(String email); boolean existsByUsername(String username); boolean existsByEmail(String email); }说明extends JpaRepositoryUser, Long提供了基础的 CRUD 方法save, findById, findAll, delete等。extends JpaSpecificationExecutorUser提供了复杂动态查询的能力。自定义的findByUsername等方法JPA 会自动实现无需编写 SQL。这就是“美好分层感”的一部分业务层无需关心 SQL 如何编写。3.4 数据传输对象层 (DTO Layer)DTO 用于在不同层之间传输数据尤其是 Controller 和外部系统之间。它解耦了内部实体和外部接口避免实体类的敏感字段如password暴露出去也方便做数据格式转换和校验。创建请求 DTO// 文件路径src/main/java/com/example/layereddemo/module/user/dto/UserDTO.java package com.example.layereddemo.module.user.dto; import lombok.Data; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; Data public class UserDTO { private Long id; // 更新时使用 NotBlank(message 用户名不能为空) Size(min 3, max 20, message 用户名长度必须在3-20之间) private String username; NotBlank(message 密码不能为空) Size(min 6, max 30, message 密码长度必须在6-30之间) private String password; NotBlank(message 昵称不能为空) private String nickname; NotBlank(message 邮箱不能为空) Email(message 邮箱格式不正确) private String email; }查询条件 DTO// 文件路径src/main/java/com/example/layereddemo/module/user/dto/UserQueryDTO.java package com.example.layereddemo.module.user.dto; import lombok.Data; Data public class UserQueryDTO { private String username; private String nickname; private String email; // 可以添加分页参数 private Integer pageNum 1; private Integer pageSize 10; }3.5 业务逻辑层 (Service Layer)这是系统的“大脑”。Service 层调用 DAO 层并封装核心业务规则。Service 接口// 文件路径src/main/java/com/example/layereddemo/module/user/service/UserService.java package com.example.layereddemo.module.user.service; import com.example.layereddemo.module.user.dto.UserDTO; import com.example.layereddemo.module.user.dto.UserQueryDTO; import com.example.layereddemo.module.user.entity.User; import org.springframework.data.domain.Page; import java.util.List; public interface UserService { /** * 创建用户 */ User createUser(UserDTO userDTO); /** * 更新用户 */ User updateUser(Long id, UserDTO userDTO); /** * 删除用户 */ void deleteUser(Long id); /** * 根据ID查询用户 */ User getUserById(Long id); /** * 条件分页查询用户列表 */ PageUser listUsers(UserQueryDTO queryDTO); /** * 检查用户名是否存在 */ boolean isUsernameExist(String username); /** * 检查邮箱是否存在 */ boolean isEmailExist(String email); }Service 实现类// 文件路径src/main/java/com/example/layereddemo/module/user/service/impl/UserServiceImpl.java package com.example.layereddemo.module.user.service.impl; import com.example.layereddemo.module.user.dao.UserDao; import com.example.layereddemo.module.user.dto.UserDTO; import com.example.layereddemo.module.user.dto.UserQueryDTO; import com.example.layereddemo.module.user.entity.User; import com.example.layereddemo.module.user.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; import java.util.Optional; Slf4j Service RequiredArgsConstructor // Lombok 注解为 final 字段生成构造函数注入 public class UserServiceImpl implements UserService { private final UserDao userDao; Override Transactional // 声明事务保证原子性 public User createUser(UserDTO userDTO) { log.info(创建用户用户名{}, userDTO.getUsername()); // 1. 业务校验 if (isUsernameExist(userDTO.getUsername())) { throw new RuntimeException(用户名已存在); } if (isEmailExist(userDTO.getEmail())) { throw new RuntimeException(邮箱已存在); } // 2. DTO 转 Entity User user new User(); BeanUtils.copyProperties(userDTO, user, id); // 忽略id由数据库生成 // 这里可以进行密码加密等操作 // user.setPassword(passwordEncoder.encode(userDTO.getPassword())); // 3. 调用 DAO 保存 return userDao.save(user); } Override Transactional public User updateUser(Long id, UserDTO userDTO) { log.info(更新用户ID{}, id); // 1. 检查用户是否存在 User existingUser getUserById(id); // 复用方法如果不存在会抛异常 // 2. 业务校验如更新时用户名/邮箱是否与他人冲突 if (StringUtils.hasText(userDTO.getUsername()) !existingUser.getUsername().equals(userDTO.getUsername()) isUsernameExist(userDTO.getUsername())) { throw new RuntimeException(用户名已被占用); } // 邮箱校验同理... // 3. 更新字段 BeanUtils.copyProperties(userDTO, existingUser, id, password); // 保护id密码单独处理 // 如果需要更新密码 // if (StringUtils.hasText(userDTO.getPassword())) { // existingUser.setPassword(passwordEncoder.encode(userDTO.getPassword())); // } // 4. 保存更新 return userDao.save(existingUser); } Override Transactional public void deleteUser(Long id) { log.info(删除用户ID{}, id); if (!userDao.existsById(id)) { throw new RuntimeException(用户不存在ID: id); } userDao.deleteById(id); } Override public User getUserById(Long id) { OptionalUser userOpt userDao.findById(id); return userOpt.orElseThrow(() - new RuntimeException(用户不存在ID: id)); } Override public PageUser listUsers(UserQueryDTO queryDTO) { // 构建动态查询条件 SpecificationUser spec (root, query, cb) - { ListPredicate predicates new ArrayList(); if (StringUtils.hasText(queryDTO.getUsername())) { predicates.add(cb.like(root.get(username), % queryDTO.getUsername() %)); } if (StringUtils.hasText(queryDTO.getNickname())) { predicates.add(cb.like(root.get(nickname), % queryDTO.getNickname() %)); } if (StringUtils.hasText(queryDTO.getEmail())) { predicates.add(cb.like(root.get(email), % queryDTO.getEmail() %)); } return cb.and(predicates.toArray(new Predicate[0])); }; // 构建分页请求 Pageable pageable PageRequest.of(queryDTO.getPageNum() - 1, queryDTO.getPageSize()); // 执行查询 return userDao.findAll(spec, pageable); } Override public boolean isUsernameExist(String username) { return userDao.existsByUsername(username); } Override public boolean isEmailExist(String email) { return userDao.existsByEmail(email); } }关键点Service标识为业务层组件。Transactional在需要进行数据库写操作的方法上添加确保事务性。业务逻辑集中所有的校验、计算、流程控制都在这里。依赖注入通过构造函数注入UserDao这是 Spring 推荐的方式。异常处理这里使用了RuntimeException简单抛出实际项目应定义业务异常体系。日志记录使用Slf4j记录关键操作日志。3.6 表现层 (Controller Layer)Controller 层是系统的“门面”负责处理 HTTP 请求和响应。// 文件路径src/main/java/com/example/layereddemo/module/user/controller/UserController.java package com.example.layereddemo.module.user.controller; import com.example.layereddemo.module.user.dto.UserDTO; import com.example.layereddemo.module.user.dto.UserQueryDTO; import com.example.layereddemo.module.user.entity.User; import com.example.layereddemo.module.user.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; RestController RequestMapping(/api/users) RequiredArgsConstructor public class UserController { private final UserService userService; PostMapping public ResponseEntityUser createUser(Valid RequestBody UserDTO userDTO) { User createdUser userService.createUser(userDTO); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } PutMapping(/{id}) public ResponseEntityUser updateUser(PathVariable Long id, Valid RequestBody UserDTO userDTO) { User updatedUser userService.updateUser(id, userDTO); return ResponseEntity.ok(updatedUser); } DeleteMapping(/{id}) public ResponseEntityVoid deleteUser(PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } GetMapping(/{id}) public ResponseEntityUser getUserById(PathVariable Long id) { User user userService.getUserById(id); return ResponseEntity.ok(user); } GetMapping public ResponseEntityPageUser listUsers(Valid UserQueryDTO queryDTO) { PageUser userPage userService.listUsers(queryDTO); return ResponseEntity.ok(userPage); } GetMapping(/check/username) public ResponseEntityBoolean checkUsername(RequestParam String username) { boolean exists userService.isUsernameExist(username); return ResponseEntity.ok(exists); } }说明RestController组合了Controller和ResponseBody直接返回 JSON 数据。RequestMapping定义 API 的基础路径。Valid触发在 DTO 上定义的校验规则如NotBlank。职责单一Controller 只做参数校验借助注解、调用 Service、返回响应。没有业务逻辑。HTTP 状态码使用ResponseEntity灵活返回状态码201 Created, 204 No Content 等。4. 运行与验证启动 Spring Boot 应用后我们可以使用 Postman 或 curl 进行测试。4.1 创建用户 (POST /api/users)请求体 (JSON):{ username: zhangsan, password: 123456, nickname: 张三, email: zhangsanexample.com }预期响应 (201 Created):{ id: 1, username: zhangsan, password: 123456, nickname: 张三, email: zhangsanexample.com, createTime: 2023-10-27T10:00:00, updateTime: 2023-10-27T10:00:00 }4.2 查询用户列表 (GET /api/users?usernamezhangpageNum1pageSize5)预期响应:{ content: [ { id: 1, username: zhangsan, ... } ], totalElements: 1, totalPages: 1, size: 5, number: 0 }可以看到我们成功实现了一个分层清晰、职责分明的 RESTful API。每一层的代码都简洁明了易于理解和测试。5. 进阶领域驱动设计DDD中的分层思想经典三层架构已经能解决大部分问题但对于复杂业务系统我们可以引入更细致的 DDD 分层思想以获得更极致的“分层感”。DDD 通常分为四层用户接口层 (Interface Layer)相当于 Controller负责展示信息和接收指令。应用层 (Application Layer)负责协调领域对象完成一个特定的业务用例如“用户注册”。它很薄不包含业务规则主要编排领域服务。领域层 (Domain Layer)这是核心中的核心包含实体 (Entity)、值对象 (Value Object)、领域服务 (Domain Service)、仓储接口 (Repository Interface)。所有的业务规则和逻辑都沉淀在这里。基础设施层 (Infrastructure Layer)提供技术实现如数据库访问实现仓储接口、消息发送、文件存储等。在我们的示例中可以将UserService中的核心业务规则如“用户名唯一性”是业务规则抽离到Domain Service中而UserServiceImpl则退化为应用服务只负责流程编排。UserDao实现 DDD 中的UserRepository接口。这种划分让领域模型独立于技术和框架更具可维护性和可测试性。6. 常见问题与排查思路在实践分层架构时你可能会遇到以下问题问题现象常见原因解决思路Bean注入失败报NoSuchBeanDefinitionException1. 类上没有加Service,Repository,Component等注解。2. 包路径不在 Spring Boot 主应用扫描范围内。1. 检查相关类是否添加了正确的注解。2. 确保你的包在SpringBootApplication主类所在包或其子包下。事务Transactional不生效1. 方法不是public。2. 异常被捕获未抛出。3. 在同一个类内部方法调用。1. 确保事务方法是public。2. 确保异常能传播出去。3. 通过注入自身代理或拆分到不同类解决。字段映射失败BeanUtils.copyProperties报错或为null1. 源对象和目标对象的字段名、类型不匹配。2. 使用了final字段。1. 检查 DTO 和 Entity 的字段定义。2. 考虑使用 MapStruct 或 ModelMapper 等专业映射工具。分页查询结果不正确1. 页码计算错误JPA 页码从0开始。2. 查询条件Specification构建逻辑有误。1. 在 Service 层正确转换页码PageRequest.of(pageNum-1, pageSize)。2. 调试Specification打印生成的 SQL 进行核对。返回的 JSON 包含不期望的字段如password实体类直接作为响应体返回序列化了所有字段。1. 使用 DTO 作为响应体只暴露必要字段。2. 或在字段上使用JsonIgnore注解。7. 最佳实践与工程建议要让分层架构的优势最大化需要遵循一些最佳实践依赖方向严格单向这是分层的铁律。依赖关系只能是Controller - Service - Dao。绝对禁止Service 调用 Controller或者 Dao 调用 Service。可以使用架构守护工具如 ArchUnit来检查。使用 DTO 进行层间通信Controller 和 Service 之间、Service 和 Service 之间尽量使用专用的 DTO 或 VOView Object来传输数据而不是直接传递 Entity。这保证了各层的独立性和数据安全性。接口与实现分离Service 层和 Dao 层都应先定义接口再提供实现。这便于后续扩展如更换缓存实现和进行 Mock 测试。异常处理规范化定义统一的业务异常类如BusinessException并在 Controller 层使用ControllerAdvice进行全局异常处理返回结构统一的错误信息。日志记录策略化在 Service 层的方法入口、出口以及关键决策点记录日志。使用Slf4j和不同的日志级别INFO, DEBUG, ERROR。测试覆盖分层进行Dao 层使用DataJpaTest进行集成测试验证数据库操作。Service 层使用MockBean模拟 Dao进行单元测试聚焦业务逻辑。Controller 层使用WebMvcTest和 MockMvc测试 HTTP 接口。包结构按模块划分如前文所示按功能模块分包如user,order,product模块内再分层。这比单纯按层分包把所有 Controller 放一起更利于功能的内聚和团队的并行开发。为复杂查询引入 Query Service对于非常复杂、涉及多表关联和聚合的查询可以单独建立一个QueryService它可能直接使用JdbcTemplate或MyBatis编写原生 SQL避免污染核心业务层的领域模型。这体现了 CQRS命令查询职责分离的思想。分层架构不是银弹但它为构建可维护、可扩展、可测试的软件系统提供了坚实的地基。从经典三层起步理解每一层的职责边界再根据项目复杂度演进到 DDD 等更精细的架构模式你就能真正体会到那种“美好的分层感”——代码结构清晰如画修改功能得心应手团队协作行云流水。