1. 为什么需要分层架构?
当你第一次接触SpringBoot项目时,可能会被各种层级的代码搞得晕头转向。为什么不能把所有代码都写在一个文件里?为什么要分成Controller、Service、Mapper这么多层?这就像盖房子不能把所有东西都堆在一个房间一样,合理的分层能让你的代码:
- 更易维护:当数据库表结构变化时,你只需要修改Mapper层,不需要动业务逻辑
- 更易测试:可以单独测试Service层的业务逻辑,不用启动整个Web容器
- 更易协作:前后端开发者可以并行工作,只需约定好接口格式
我在实际项目中见过一个反例:所有SQL都写在Controller里,结果需求变更时要改20多个文件。而合理的分层后,同样的变更只需要改2个文件。
2. 用户管理系统实战设计
让我们以用户管理系统为例,设计一个典型的分层结构:
src/main/java ├── com.example.user │ ├── config # 配置类 │ ├── controller # API接口层 │ ├── service # 业务逻辑层 │ │ ├── impl # 服务实现 │ ├── mapper # 数据访问层 │ ├── model # 数据模型 │ │ ├── dto # 数据传输对象 │ │ ├── entity # 数据库实体 │ │ └── vo # 视图对象 └── resources ├── mapper # MyBatis映射文件这种结构看起来复杂,但实际开发中会带来巨大便利。比如当需要添加用户权限功能时,你清楚地知道该在哪个目录下新建文件。
3. 从数据库到API的完整实现
3.1 实体层(Entity)设计
首先设计用户表对应的实体类,这里推荐使用Lombok简化代码:
@Data @TableName("sys_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private String email; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.UPDATE) private LocalDateTime updateTime; }几个关键点:
@TableName指定对应的数据库表名@TableId标记主键,IdType.AUTO表示自增@TableField的fill属性可以实现自动填充
3.2 Mapper层实现
MyBatis-Plus可以极大简化Mapper代码:
public interface UserMapper extends BaseMapper<User> { // 自定义复杂查询 @Select("SELECT * FROM sys_user WHERE email LIKE '%#{domain}'") List<User> selectByEmailDomain(String domain); }BaseMapper已经提供了基本的CRUD方法,你只需要添加特殊查询即可。我建议复杂查询还是写在XML里更清晰。
3.3 Service层业务封装
Service层应该体现业务逻辑,而不是直接暴露数据库操作:
public interface UserService { UserDTO createUser(CreateUserDTO dto); Page<UserVO> queryUsers(UserQuery query); } @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; @Override @Transactional public UserDTO createUser(CreateUserDTO dto) { if (userMapper.existsByUsername(dto.getUsername())) { throw new BusinessException("用户名已存在"); } User user = new User(); BeanUtils.copyProperties(dto, user); user.setPassword(encodePassword(dto.getPassword())); userMapper.insert(user); return convertToDTO(user); } }注意这里使用了DTO进行层间数据传输,避免实体类直接暴露给前端。
3.4 Controller层API设计
Controller应该保持精简,只处理HTTP相关逻辑:
@RestController @RequestMapping("/users") @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping public Result<UserDTO> createUser(@Valid @RequestBody CreateUserDTO dto) { return Result.success(userService.createUser(dto)); } @GetMapping public Result<Page<UserVO>> queryUsers(UserQuery query) { return Result.success(userService.queryUsers(query)); } }4. 分层开发中的常见陷阱
4.1 贫血模型问题
很多项目中的Entity只有getter/setter,这被称为贫血模型。更好的做法是将相关行为封装到实体中:
public class User { // ... 字段定义 public void changePassword(String oldPass, String newPass) { if (!matchPassword(oldPass)) { throw new IllegalStateException("旧密码错误"); } this.password = encodePassword(newPass); } }4.2 跨层调用问题
绝对不要出现Controller直接调用Mapper的情况!这会导致业务逻辑无处安放。正确的调用链应该是:
Controller -> Service -> Mapper4.3 循环依赖问题
如果UserService调用OrderService,而OrderService又需要调用UserService,就会形成循环依赖。解决方案:
- 提取公共逻辑到第三个Service
- 使用事件驱动架构
- 将部分逻辑下沉到领域对象中
5. 进阶技巧:DTO与MapStruct
随着项目复杂度的提升,你会发现实体类与DTO之间的转换代码非常冗长。这时候可以使用MapStruct自动生成映射代码:
@Mapper public interface UserConverter { UserConverter INSTANCE = Mappers.getMapper(UserConverter.class); @Mapping(source = "createTime", target = "registerDate") UserDTO toDTO(User user); } // 使用方式 UserDTO dto = UserConverter.INSTANCE.toDTO(user);MapStruct会在编译时生成实现类,性能远高于反射实现的BeanUtils。
6. 测试策略
分层架构的一个巨大优势是便于测试:
- Mapper层:使用MyBatis Test或直接集成测试
- Service层:Mockito模拟依赖进行单元测试
- Controller层:MockMvc模拟HTTP请求
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldCreateUser() throws Exception { when(userService.createUser(any())).thenReturn(new UserDTO()); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"test\"}")) .andExpect(status().isOk()); } }7. 项目结构优化建议
当项目规模扩大后,可以按功能模块进一步拆分:
com.example ├── module1 │ ├── controller │ ├── service │ └── mapper └── module2 ├── controller └── service每个模块可以独立打包,通过maven或gradle管理依赖关系。