1. 这篇文章真正要解决的问题
“分层感”这个词,听起来有点抽象,甚至带点文艺气息。但在软件开发的世界里,它不是一个审美概念,而是一个关乎项目生死存亡的工程实践。你有没有遇到过这样的场景:接手一个老项目,想改个简单的用户信息展示逻辑,却发现要动数据库连接、改业务逻辑、甚至影响前端渲染,牵一发而动全身?或者,自己写的代码几个月后回头看,已经看不懂当初为什么要把网络请求、数据解析和界面刷新全塞在一个文件里?
这些痛苦的根源,就是缺乏清晰、合理的分层。本文要解决的,正是这个困扰无数开发者的核心问题:如何通过架构分层,构建出易于理解、易于维护、易于扩展的软件系统。这不仅仅是“写代码”,而是“设计代码”。很多人以为分层只是 MVC(Model-View-Controller)那么简单,但实际上,真正的分层设计远不止于此,它关乎职责分离、依赖管理、测试策略和团队协作效率。
本文将从一个具体的技术实现角度切入,为你拆解“美好的分层感”在实战中究竟意味着什么。你会看到,一个清晰的分层架构如何将混乱的“面条式代码”转化为模块清晰、职责分明的“乐高积木”。我们不止讲概念,更会通过一个完整的 Spring Boot + MyBatis 后端服务示例,展示从数据访问层、业务逻辑层到接口层的具体实现、代码规范以及那些新手最容易踩的坑。读完本文,你将能清晰地回答:我的项目应该分几层?每层到底该做什么?层与层之间如何优雅地交互?从而真正获得那种让代码结构清晰、让开发心情愉悦的“分层感”。
2. 基础概念:什么是软件架构分层?
在深入代码之前,我们必须统一思想:分层到底是什么,以及为什么要分层。
分层的本质是“关注点分离”。想象一下建造一栋房子,电工负责布线,水管工负责管道,结构工程师负责承重,设计师负责外观。如果他们全都混在一起工作,会是一场灾难。软件分层也是同理,我们将一个复杂的软件系统,按照不同的职责和抽象级别,垂直切割成若干个相对独立的层次。每一层都像一个专业的工人,只专注于自己的领域,并通过定义好的接口与上下层协作。
一个典型的后端应用分层架构(以经典三层架构为例)如下:
| 层级 | 核心职责 | 类比 | 关键原则 |
|---|---|---|---|
| 表现层 (Presentation Layer) | 处理外部请求与响应。接收HTTP请求,解析参数,调用业务层,并封装数据返回(JSON/XML等)。 | 餐厅服务员:接待顾客、点单、传菜、结账。不关心菜怎么做。 | 薄,仅做协议转换、数据校验(如@Valid)、简单组装。 |
| 业务逻辑层 (Business Logic Layer / Service Layer) | 实现核心业务规则与流程。它是系统的“大脑”,协调多个数据操作完成一个业务用例。 | 厨师与后厨管理:根据订单(需求),指挥备菜、烹饪、摆盘。包含所有菜谱(业务规则)。 | 保持纯粹的业务逻辑,不应包含数据访问细节(如SQL)或HTTP相关代码。 |
| 数据访问层 (Data Access Layer / Persistence Layer) | 负责与数据源(数据库、缓存、外部API)交互。执行CRUD(增删改查)操作。 | 采购与仓库管理员:负责从市场(数据库)获取原材料(数据),并存储加工后的成品。 | 隐藏数据源的实现细节(是用MySQL还是Redis),向上提供统一的领域对象操作接口。 |
| (可选) 领域模型层 (Domain Model Layer) | 承载核心业务概念和状态。包含实体(Entity)、值对象(Value Object)、领域服务等。 | 菜单与食材标准:定义了“宫保鸡丁”需要哪些原料、什么口味,是业务的核心描述。 | 贫血模型(仅含getter/setter)是常见误区,应努力向富血模型(包含行为)演进。 |
为什么这种“分层感”是美好的?
- 高可维护性:修改数据库从MySQL换到PostgreSQL?你只需要改动数据访问层的实现,业务层和表现层完全不受影响。
- 高可测试性:业务逻辑层可以轻松地通过Mock数据访问层进行单元测试,无需启动整个数据库和Web容器。
- 团队协作清晰:前端工程师与表现层(Controller)对接,业务开发专注于Service,DBA或资深后端可以优化数据访问层,并行不悖。
- 代码复用:相同的业务逻辑(Service)可以被不同的表现层(Web API, RPC接口,定时任务)复用。
新手最大的误区是认为“用了Spring Boot和MyBatis就等于分层了”。实际上,框架只是工具,混乱的代码放在框架里依然是混乱的。真正的分层是一种设计约束和编码纪律。
3. 环境准备与项目初始化
接下来,我们将通过一个具体的项目——“用户信息管理系统”——来实践分层架构。这个项目将提供用户的增删改查(CRUD)接口。
技术栈选型:
- Java 17+:LTS版本,提供现代语言特性。
- Spring Boot 3.x:快速构建企业级应用的脚手架。
- MyBatis-Plus 3.5.x:强大的MyBatis增强工具,简化CRUD操作。
- MySQL 8.0:关系型数据库。
- Maven:项目管理与构建工具。
- Lombok:通过注解简化Java Bean的编写。
初始化项目:你可以通过 Spring Initializr 快速生成项目,或使用以下Mavenpom.xml核心依赖。
<?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>3.1.5</version> <!-- 请使用最新稳定版 --> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>layered-architecture-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>layered-architecture-demo</name> <description>Demo project for Layered Architecture</description> <properties> <java.version>17</java.version> <mybatis-plus.version>3.5.4</mybatis-plus.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>数据库准备:在MySQL中创建数据库和表。
CREATE DATABASE IF NOT EXISTS `user_demo` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `user_demo`; CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `age` int DEFAULT NULL COMMENT '年龄', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';配置文件application.yml:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/user_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: your_username # 替换为你的数据库用户名 password: your_password # 替换为你的数据库密码 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境请关闭 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名(如果启用) logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值环境准备好后,我们开始从最底层向上,逐层构建我们的应用。
4. 数据访问层(DAO/Mapper层)实现
数据访问层是系统的基石,它直接与数据库打交道。我们的目标是:对外隐藏所有SQL和数据库连接细节,向上提供纯粹的领域对象操作接口。
第一步:创建实体类(Entity)实体类对应数据库表,它属于领域模型的一部分,通常放在entity包下。我们使用 Lombok 简化代码。
// 文件路径:src/main/java/com/example/demo/entity/User.java package com.example.demo.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("user") // 指定对应表名 public class User { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private String username; private String email; private Integer age; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }关键点:@TableField注解配合 MetaObjectHandler 可以自动填充时间,避免在每个业务代码中手动设置。
第二步:创建Mapper接口Mapper接口定义了数据操作的方法,MyBatis-Plus 会为其生成代理实现。它应放在mapper包下。
// 文件路径:src/main/java/com/example/demo/mapper/UserMapper.java package com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper // 关键注解,让Spring管理此接口并生成代理Bean public interface UserMapper extends BaseMapper<User> { // 继承 BaseMapper<User> 后,已经拥有了基础的CRUD方法: // insert(T entity), deleteById(Serializable id), updateById(T entity), // selectById(Serializable id), selectList(Wrapper<T> queryWrapper) 等。 // 复杂查询可以在此定义。 }这就是分层的好处:业务层只需要调用userMapper.selectById(1L),完全不用关心SQL怎么写、连接池如何管理。数据访问层的变更被隔离在此处。
第三步(可选):配置MetaObjectHandler实现自动填充为了自动处理createTime和updateTime,我们需要一个配置类。
// 文件路径:src/main/java/com/example/demo/config/MyMetaObjectHandler.java package com.example.demo.config; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }至此,数据访问层搭建完毕。它干净、职责单一,只做“数据存取”这一件事。
5. 业务逻辑层(Service层)实现
业务逻辑层是系统的核心,它负责协调多个数据操作,并封装具体的业务规则。一个常见的坏味道是“事务脚本”,即把业务逻辑全写在Service方法里,变成面向过程的代码。好的Service层应该是面向领域的。
第一步:创建Service接口接口定义契约,有利于实现类的替换(例如用于测试的Mock实现)和更清晰的职责声明。
// 文件路径:src/main/java/com/example/demo/service/UserService.java package com.example.demo.service; import com.example.demo.entity.User; import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IService<User> { // IService提供了大量通用方法,我们也可以定义自己的业务方法 /** * 注册新用户(包含业务校验) * @param user 用户信息 * @return 是否成功 */ boolean registerUser(User user); /** * 根据用户名查询用户详情(可能包含复杂组装逻辑) * @param username 用户名 * @return 用户详情DTO */ UserDetailDTO getUserDetailByUsername(String username); }第二步:创建数据传输对象(DTO)为什么需要DTO?因为前端需要的字段和数据库实体(Entity)的字段往往不是一一对应的。直接暴露Entity会给前端带来不必要的字段,也可能暴露敏感信息(如密码哈希)。DTO是层与层之间数据传输的载体,属于表现层或业务层的概念。
// 文件路径:src/main/java/com/example/demo/service/dto/UserDetailDTO.java package com.example.demo.service.dto; import lombok.Data; import java.time.LocalDateTime; @Data public class UserDetailDTO { private Long id; private String username; private String email; private Integer age; private LocalDateTime createTime; // 可以包含Entity中没有的字段,例如: // private List<OrderDTO> recentOrders; // 用户最近订单 }第三步:实现Service实现类注入Mapper,并实现具体的业务逻辑。注意事务控制。
// 文件路径:src/main/java/com/example/demo/service/impl/UserServiceImpl.java package com.example.demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import com.example.demo.service.dto.UserDetailDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @Slf4j @RequiredArgsConstructor // Lombok注解,为final字段生成构造函数注入 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 推荐构造函数注入,字段声明为final private final UserMapper userMapper; @Override @Transactional(rollbackFor = Exception.class) // 声明式事务,发生任何异常都回滚 public boolean registerUser(User user) { // 1. 业务校验 if (user == null || !StringUtils.hasText(user.getUsername())) { log.warn("注册用户失败:用户信息为空或用户名为空"); return false; } // 检查用户名是否已存在 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, user.getUsername()); Long count = userMapper.selectCount(wrapper); if (count > 0) { log.warn("注册用户失败:用户名 {} 已存在", user.getUsername()); return false; } // 2. 设置默认值或执行其他业务逻辑(例如密码加密) // user.setPassword(passwordEncoder.encode(user.getPassword())); // 3. 调用数据访问层保存 int result = userMapper.insert(user); // 4. 后续业务(例如发送欢迎邮件、初始化用户配置等) // sendWelcomeEmail(user.getEmail()); return result > 0; } @Override public UserDetailDTO getUserDetailByUsername(String username) { if (!StringUtils.hasText(username)) { return null; } LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User user = userMapper.selectOne(wrapper); if (user == null) { return null; } // 实体转DTO UserDetailDTO dto = new UserDetailDTO(); BeanUtils.copyProperties(user, dto); // 这里可以组装更复杂的DTO,例如调用其他Service查询订单信息 // dto.setRecentOrders(orderService.getRecentOrders(user.getId())); return dto; } }这一层是“分层感”的灵魂:它包含了“用户名不能重复”的业务规则,以及“注册用户”这个用例的完整流程(校验->保存->后续操作)。所有数据操作细节都被封装在Mapper调用之后。
6. 表现层(Controller层)实现
表现层是系统的门面,负责处理HTTP请求和响应。它的职责应该尽可能“薄”:解析参数、校验数据、调用Service、封装结果。切忌在Controller里写业务逻辑或复杂的数据库查询。
第一步:创建请求/响应对象同样,为了不污染Entity,我们为API接口定义专用的请求和响应对象。
// 文件路径:src/main/java/com/example/demo/controller/request/CreateUserRequest.java package com.example.demo.controller.request; import lombok.Data; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.PositiveOrZero; @Data public class CreateUserRequest { @NotBlank(message = "用户名不能为空") private String username; @Email(message = "邮箱格式不正确") private String email; @NotNull(message = "年龄不能为空") @PositiveOrZero(message = "年龄必须大于等于0") private Integer age; }// 文件路径:src/main/java/com/example/demo/controller/response/ApiResponse.java package com.example.demo.controller.response; import lombok.Data; @Data public class ApiResponse<T> { private Integer code; private String message; private T data; public static <T> ApiResponse<T> success(T data) { ApiResponse<T> response = new ApiResponse<>(); response.setCode(200); response.setMessage("success"); response.setData(data); return response; } public static ApiResponse<?> error(Integer code, String message) { ApiResponse<?> response = new ApiResponse<>(); response.setCode(code); response.setMessage(message); return response; } }第二步:实现ControllerController调用Service,并处理异常和返回格式。
// 文件路径:src/main/java/com/example/demo/controller/UserController.java package com.example.demo.controller; import com.example.demo.controller.request.CreateUserRequest; import com.example.demo.controller.response.ApiResponse; import com.example.demo.entity.User; import com.example.demo.service.UserService; import com.example.demo.service.dto.UserDetailDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") @Slf4j @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping public ApiResponse<Long> createUser(@RequestBody @Validated CreateUserRequest request) { log.info("创建用户请求: {}", request); // 1. Request 转 Entity (这里简单转换,复杂情况可用MapStruct) User user = new User(); BeanUtils.copyProperties(request, user); // 2. 调用业务层 boolean success = userService.registerUser(user); // 3. 处理响应 if (success) { return ApiResponse.success(user.getId()); } else { return ApiResponse.error(400, "用户创建失败,可能用户名已存在"); } } @GetMapping("/{username}") public ApiResponse<UserDetailDTO> getUserByUsername(@PathVariable String username) { log.info("查询用户请求,用户名: {}", username); UserDetailDTO userDetail = userService.getUserDetailByUsername(username); if (userDetail != null) { return ApiResponse.success(userDetail); } else { return ApiResponse.error(404, "用户不存在"); } } // 全局异常处理可以放在 @ControllerAdvice 中,这里为简化写在Controller内 @ExceptionHandler(Exception.class) public ApiResponse<?> handleException(Exception e) { log.error("Controller发生异常: ", e); return ApiResponse.error(500, "服务器内部错误: " + e.getMessage()); } }Controller层干净利落:它只关心HTTP协议、参数校验和响应格式。所有的业务复杂性都委托给了Service层。这就是分层带来的清晰边界。
7. 运行、测试与效果验证
启动应用:在项目根目录下执行:
mvn spring-boot:run看到Started LayeredArchitectureDemoApplication in X.XXX seconds即表示启动成功。
接口测试:使用 Postman 或 curl 进行测试。
创建用户 (POST
http://localhost:8080/api/users)curl -X POST http://localhost:8080/api/users \ -H "Content-Type: application/json" \ -d '{"username":"testuser","email":"test@example.com","age":25}'预期成功响应:
{ "code": 200, "message": "success", "data": 1 // 返回新用户的ID }预期失败响应(重复用户名):
{ "code": 400, "message": "用户创建失败,可能用户名已存在", "data": null }查询用户 (GET
http://localhost:8080/api/users/testuser)curl http://localhost:8080/api/users/testuser预期响应:
{ "code": 200, "message": "success", "data": { "id": 1, "username": "testuser", "email": "test@example.com", "age": 25, "createTime": "2023-10-27T10:30:00", "updateTime": "2023-10-27T10:30:00" } }
验证分层效果:
- 查看控制台日志,MyBatis-Plus 会打印出执行的SQL语句,这证明了数据访问层在工作。
- 尝试在
CreateUserRequest中传入一个非法邮箱或负年龄,观察@Validated注解的校验是否生效(需要配合@ControllerAdvice全局处理,本例简化了)。 - 在
UserServiceImpl.registerUser方法中打上断点,观察业务逻辑的执行流程。
当你能清晰地追踪一个HTTP请求如何流经Controller -> Service -> Mapper -> Database,并最终带着结果返回时,你就真切地感受到了“分层”带来的秩序和掌控感。
8. 常见问题、坑点与排查思路
即使遵循了分层架构,在实际开发中依然会遇到各种问题。下面是一些典型场景和解决方案。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Service中注入的Mapper为null | 1. Mapper接口缺少@Mapper注解。2. Service实现类未加 @Service注解。3. 包扫描路径问题,Spring未扫描到Mapper。 | 1. 检查Mapper接口注解。 2. 检查启动类上的 @MapperScan注解路径是否正确。3. 查看启动日志是否有Bean创建失败信息。 | 1. 确保注解正确。 2. 在启动类添加 @MapperScan("com.example.demo.mapper")。3. 使用 @Autowired(required=false)测试是否注入成功。 |
事务@Transactional不生效 | 1. 方法非public。 2. 异常被catch未抛出。 3. 自调用(同一个类中A方法调用B方法,B方法有@Transactional)。 4. 数据库引擎不支持事务(如MyISAM)。 | 1. 检查方法修饰符。 2. 检查代码中是否有 try-catch吞了异常。3. 检查调用链。 4. 检查数据库表引擎。 | 1. 确保方法是public。 2. 在catch块中抛出 RuntimeException或使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。3. 将事务方法放到另一个Service类中调用,或使用AopContext。 4. 将表引擎改为InnoDB。 |
| Entity与数据库字段映射失败 | 1. 实体类字段名与数据库列名不一致(下划线转驼峰默认开启,但仍需注意)。 2. 使用了MySQL关键字作为字段名。 3. JPA/MyBatis配置问题。 | 1. 查看MyBatis日志,对比生成的SQL和实际表结构。 2. 检查实体类 @TableField注解的value属性。 | 1. 在application.yml中配置mybatis-plus.configuration.map-underscore-to-camel-case: true。2. 对于关键字字段,使用反引号 `order`或在注解中指定@TableField("order")。3. 使用 @TableField(value = "db_column_name")显式指定映射。 |
| 循环依赖(A Service注入B Service,B又注入A) | 架构设计问题,职责不清晰。 | 启动时报错:The dependencies of some of the beans in the application context form a cycle。 | 1.最佳方案:重构代码,提取公共逻辑到第三个Service或Util类中,打破循环。 2.临时方案:在其中一个注入点使用 @Lazy注解,延迟加载。但这是治标不治本。 |
| DTO/Request/Entity转换代码冗长 | 手动使用BeanUtils.copyProperties或 setter 方法逐个赋值。 | 查看Service和Controller中大量的属性拷贝代码。 | 引入对象映射工具,如MapStruct(编译时生成代码,零运行时开销)或ModelMapper(运行时反射,较灵活但性能稍差)。 |
| 层与层之间参数传递混乱 | 图省事,在不同层之间直接传递Entity、Map或大量基本类型参数。 | 查看方法签名,如果参数超过5个,或在不同层传递了不合适的对象(如Controller直接收到Entity)。 | 严格定义各层的数据契约:Controller和Service之间用Request/Response DTO;Service和Mapper之间用Entity或参数对象。保持接口简洁、语义明确。 |
9. 最佳实践与工程化建议
要让“分层感”不仅停留在Demo,而是融入实际工程,你需要遵循以下最佳实践:
包结构规划:
com.example.demo ├── application # (可选) 应用层,协调用例 ├── controller │ ├── request # 请求对象 │ ├── response # 响应对象 │ └── v1 # 接口版本化管理 ├── service │ ├── dto # 服务间传输对象 │ ├── impl │ └── converter # 对象转换器 ├── domain # 领域层(核心) │ ├── entity # 领域实体 │ ├── vo # 值对象 │ ├── repository # 领域仓储接口(对应Mapper) │ └── service # 领域服务(纯业务逻辑) ├── infrastructure # 基础设施层 │ ├── mapper # MyBatis Mapper (实现domain层的repository) │ ├── config │ └── util # 通用工具 └── LayeredArchitectureDemoApplication.java这是更清晰的领域驱动设计(DDD)分包方式,将业务核心(domain)与技术细节(infrastructure)分离。
使用MapStruct进行对象映射:在
pom.xml中添加依赖和插件,定义UserMapper接口,MapStruct会在编译时生成UserMapperImpl,实现User到UserDetailDTO等的高效、类型安全转换,彻底告别手写BeanUtils.copyProperties。统一异常处理:创建
GlobalExceptionHandler类,使用@ControllerAdvice和@ExceptionHandler集中处理校验异常、业务异常和系统异常,保证Controller层干净,且API错误响应格式统一。接口版本化管理:在Controller层使用路径版本化,如
@RequestMapping("/api/v1/users"),为后续不兼容的API升级留出空间。为Service编写单元测试:利用
@SpringBootTest进行集成测试,或使用Mockito对Service进行纯单元测试,Mock掉Mapper依赖,确保业务逻辑的正确性。这是分层架构可测试性的直接体现。明确各层职责,禁止跨层调用:
- Controller不能直接调用Mapper。
- Service不能处理HTTP请求响应。
- Mapper只能做数据操作,不能有业务逻辑。 严格遵守依赖方向:Controller -> Service -> Mapper。高层模块依赖低层模块的抽象。
日志记录:在Service层的关键业务节点和异常处使用
log对象记录日志,便于问题追踪。区分INFO,WARN,ERROR级别。
当你将这些实践应用到项目中,每一次代码的增删改查都会变得有迹可循、有条不紊。那种因为代码结构清晰而带来的愉悦和效率提升,就是“美好的分层感”最实在的回报。它让编程从一种体力劳动,更多地转变为一种设计艺术。