1. 项目背景与核心价值
去年接手一个后台管理系统重构项目时,我深刻体会到三层架构在业务逻辑解耦上的重要性。当时旧系统各种Service层直接操作数据库,一个简单的用户信息变更需要改5个文件,测试覆盖率不到30%。采用标准的三层设计模式重构后,不仅代码量减少40%,单元测试覆盖率直接提升到85%以上。
这个基于Spring Boot的用户CRUD示例,正是我当时重构方案的简化版。别看只是基础的增删查改,这里面包含了企业级开发中最关键的几个设计要点:
- 清晰的职责边界(Controller只处理HTTP协议)
- 可测试的业务逻辑(Service层完全无状态)
- 持久层隔离(MyBatis/Dao层实现细节封装)
2. 技术栈选型解析
2.1 为什么选择Spring Boot
相比传统SSM框架的复杂配置,Spring Boot的自动装配特性让开发者能快速搭建可运行的应用骨架。实测从零开始搭建这个项目,只需要:
# 使用Spring Initializr生成项目 curl https://start.spring.io/starter.zip \ -d dependencies=web,mybatis,mysql \ -d type=gradle-project \ -d language=java \ -d bootVersion=3.1.5 \ -o crud-demo.zip关键依赖说明:
spring-boot-starter-web:提供嵌入式Tomcat和MVC支持mybatis-spring-boot-starter:集成MyBatis持久层框架mysql-connector-j:MySQL官方JDBC驱动
2.2 数据库设计考量
用户表设计遵循三个原则:
- 字段非空约束(NOT NULL)
- 索引优化(对username建立唯一索引)
- 适度冗余(如存储加密后的密码而非明文)
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL COMMENT '登录账号', `password` varchar(128) NOT NULL COMMENT '加密密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `mobile` varchar(20) DEFAULT NULL COMMENT '联系电话', `status` tinyint DEFAULT '1' COMMENT '状态(0:禁用,1:正常)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 三层架构具体实现
3.1 Controller层设计
遵循RESTful风格的同时,需要注意三个要点:
- 使用
@Validated进行参数校验 - 统一返回格式(包含code/message/data)
- 异常处理(通过
@ControllerAdvice全局捕获)
典型用户查询接口实现:
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public Result<UserVO> getById(@PathVariable Long id) { return Result.success(userService.getUserById(id)); } // 其他CRUD接口... }关键经验:Controller方法应保持"瘦身",单个方法代码行数建议不超过15行
3.2 Service层业务逻辑
服务层需要特别注意事务管理。比如用户创建流程:
@Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; @Transactional(rollbackFor = Exception.class) @Override public Long createUser(UserCreateDTO dto) { // 1. 用户名唯一性校验 if (userMapper.existsByUsername(dto.getUsername())) { throw new BusinessException("用户名已存在"); } // 2. DTO转Entity User user = new User(); BeanUtils.copyProperties(dto, user); user.setPassword(PasswordUtil.encrypt(dto.getPassword())); // 3. 持久化操作 userMapper.insert(user); return user.getId(); } }3.3 DAO层实现技巧
MyBatis使用建议:
- 使用
@Mapper注解而非XML配置(简化开发) - 动态SQL用注解方式实现
- 复杂查询使用ResultMap映射
示例查询方法:
@Mapper public interface UserMapper { @Select("SELECT * FROM sys_user WHERE id = #{id}") @Results(id = "userMap", value = { @Result(property = "realName", column = "real_name"), @Result(property = "createTime", column = "create_time") }) User selectById(Long id); @SelectProvider(type = UserSqlProvider.class, method = "selectByCondition") List<User> selectByCondition(UserQuery query); }4. 关键问题解决方案
4.1 密码安全处理
常见误区是直接MD5加密存储。推荐方案:
- 使用BCryptPasswordEncoder(Spring Security提供)
- 每次加密生成随机盐值
- 验证时自动处理盐值匹配
public class PasswordUtil { private static final PasswordEncoder encoder = new BCryptPasswordEncoder(); public static String encrypt(String rawPassword) { return encoder.encode(rawPassword); } public static boolean matches(String raw, String encoded) { return encoder.matches(raw, encoded); } }4.2 分页查询优化
避免内存分页(先查全量再截取),推荐MyBatis分页插件:
@GetMapping public Result<PageResult<UserVO>> listUsers( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { PageHelper.startPage(page, size); List<User> users = userService.listUsers(); PageInfo<User> pageInfo = new PageInfo<>(users); return Result.success(PageResult.of( pageInfo.getList(), pageInfo.getTotal())); }5. 测试策略建议
5.1 单元测试重点
Service层测试要点:
- 使用内存数据库H2
- 每个测试用例独立事务
- 验证业务异常场景
@SpringBootTest @Transactional class UserServiceTest { @Autowired private UserService userService; @Test void createUser_shouldFailWhenUsernameExists() { UserCreateDTO dto = new UserCreateDTO(); dto.setUsername("admin"); assertThrows(BusinessException.class, () -> userService.createUser(dto)); } }5.2 API测试技巧
使用TestRestTemplate进行端点测试:
@Test void getUser_shouldReturn200() { ResponseEntity<UserVO> response = restTemplate.getForEntity( "/api/user/1", UserVO.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody().getUsername()); }6. 生产级改进方向
- 审计日志增强:通过AOP记录操作日志
@Aspect @Component public class AuditLogAspect { @AfterReturning( pointcut = "execution(* com.example..*Service.*(..))", returning = "result") public void logServiceAccess(JoinPoint jp, Object result) { // 记录方法调用和结果... } }- 接口幂等处理:对于修改操作添加token机制
- 数据权限控制:通过ThreadLocal传递租户/部门信息
三层架构看似简单,但在实际开发中,我见过太多项目因为层间职责模糊导致的维护噩梦。清晰的层级划分就像城市的地下管网系统——平时看不见,但决定了整个系统的扩展性和稳定性。建议新手开发者从这个小项目入手,体会每层应该做什么、不应该做什么,这种设计思维会伴随你的整个开发生涯。