最标准的SpringBoot三层架构(Controller-Service-DAO)和两个关键的辅助层的全盘梳理。
一、整体分层图景(先看全貌)
text
┌─────────────────────────────────────────────────────┐ │ 前端 / 第三方调用 │ └──────────────────────┬──────────────────────────────┘ │ JSON/HTTP ▼ ┌─────────────────────────────────────────────────────┐ │ 【Controller 层】 aka Web层 / 接口层 │ │ 职责:接收请求、参数校验、返回响应 │ │ 注解:@RestController, @RequestMapping │ └──────────────────────┬──────────────────────────────┘ │ 调用 ▼ ┌─────────────────────────────────────────────────────┐ │ 【Service 层】 aka 业务层 │ │ 职责:核心业务逻辑、事务管理、缓存处理 │ │ 注解:@Service, @Transactional │ └──────────────────────┬──────────────────────────────┘ │ 调用 ▼ ┌─────────────────────────────────────────────────────┐ │ 【DAO/Mapper 层】 aka 数据访问层 │ │ 职责:与数据库交互,执行SQL │ │ 注解:@Mapper / @Repository │ └──────────────────────┬──────────────────────────────┘ │ ▼ [数据库]
但实际项目中,远不止这三层,下面我给你展开真正的“企业级分层”。
二、五层详解(由外到内)
1. Controller 层(接口层 / Web层)
定位:项目的“大门”,是所有外部请求的统一入口。
该做的事:
接收HTTP请求,解析请求参数(
@RequestParam、@PathVariable、@RequestBody)。做最基本的参数校验(非空、格式),但不做业务逻辑校验。
调用Service层处理业务。
把Service返回的结果封装成统一响应格式(如
Result<T>)返回给前端。
不该做的事:
❌ 写任何业务逻辑(比如计算价格、判断状态机)。
❌ 直接操作数据库(不能出现
xxxMapper的注入)。❌ 处理异常时把堆栈信息直接抛给前端(要全局捕获并转换)。
代码范例:
java
@RestController @RequestMapping("/api/user") @Slf4j public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result<String> login(@Valid @RequestBody LoginRequest request) { // 只做调用和封装,不做业务判断 String token = userService.login(request.getUsername(), request.getPassword()); return Result.success(token); } }2. Service 层(业务层)——最核心
定位:整个项目的大脑,所有业务规则都在这里落地。
该做的事:
实现具体的业务逻辑(比如下单时先查库存、再扣库存、再生成订单)。
管理事务(
@Transactional),保证数据一致性。处理缓存(
@Cacheable)、异步任务(@Async)。调用多个DAO层完成复杂的数据聚合。
抛出业务异常(
BizException),由全局异常处理器统一处理。
不该做的事:
❌ 处理HTTP相关的东西(
HttpServletRequest、HttpServletResponse)。❌ 直接返回JSON给前端(那是Controller的事)。
❌ 在Service里写SQL或操作数据库底层细节。
设计规范:接口 + 实现类是标准写法(虽然Spring可以直接用实现类,但接口利于解耦和单元测试)。
java
// 接口 public interface UserService { String login(String username, String password); } // 实现类 @Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @Transactional(rollbackFor = Exception.class) public String login(String username, String password) { // 1. 查询用户 User user = userMapper.selectByUsername(username); // 2. 业务校验(密码比对) if (user == null || !password.equals(user.getPassword())) { throw new BizException("用户名或密码错误"); } // 3. 生成Token(业务逻辑) return JwtUtil.generateToken(user.getId()); } }3. DAO/Mapper 层(数据访问层)
定位:数据库的“翻译官”,把Java对象和数据库表进行映射。
该做的事:
定义SQL语句(或通过MyBatis-Plus的BaseMapper自动生成)。
执行CRUD操作,返回PO(持久化对象,即数据库表对应的实体类)。
仅做数据访问,不包含任何业务判断。
不该做的事:
❌ 写业务逻辑(比如
if判断状态然后改SQL,这是Service的活)。❌ 处理异常后返回特殊值(比如返回
null,让Service去处理)。
代码范例(MyBatis-Plus风格):
java
@Mapper // MyBatis的注解,交给Spring管理 public interface UserMapper extends BaseMapper<User> { // 复杂查询自己写XML或注解SQL @Select("SELECT * FROM user WHERE username = #{username}") User selectByUsername(String username); }4. DTO / VO 层(数据传输层)——容易被忽略但极其重要
定位:专门用来“传递数据”的对象,绝不与数据库表结构强绑定。
为什么要单独拎出来?
PO(实体类):和数据库表一一对应,字段完全一致。但你不能直接把它暴露给前端,因为:
用户密码、敏感字段不能返回。
前端可能只需要5个字段,但你查了全表50个字段,浪费性能。
接口升级时,改数据库表结构会影响所有调用方。
正确做法:
Request(请求DTO):前端传参用的对象,比如
LoginRequest。Response(响应VO):返回给前端的对象,比如
UserVO,只包含前端需要的字段。
java
// 请求对象 @Data public class LoginRequest { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; } // 响应对象(不返回密码) @Data public class UserVO { private Long id; private String username; private String phone; }5. 异常处理层(全局统一处理)——项目稳定的压舱石
定位:不是传统意义上的业务层,但每个成熟项目都必须有。
作用:用@ControllerAdvice统一捕获所有异常,把技术异常(如空指针)转换成前端能看懂的业务错误码。
java
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 处理自定义业务异常 @ExceptionHandler(BizException.class) public Result<String> handleBizException(BizException e) { log.warn("业务异常:{}", e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<String> handleValidException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); return Result.error(400, msg); } }三、数据流转全链路(串起来理解)
我给你模拟一个用户登录的完整数据流转,你就全明白了:
前端发来JSON:
{"username":"张三","password":"123"}。Controller层接收 → 用
LoginRequest接参,@Valid自动校验非空。Controller层调用
userService.login(),把username和password传过去。Service层:
调用
userMapper.selectByUsername()查询数据库。拿到
User对象(包含加密密码)。进行密码比对,失败则抛出
BizException。成功则生成JWT Token。
Service层把Token返回给Controller。
Controller层把Token封装成
Result.success(token),转成JSON返回前端。如果中间任何环节出错:全局异常处理器拦截,返回
Result.error()。
四、“各层注入关系禁忌表”(面试高频坑)
| 层 | 可以注入谁 | 绝对不能注入谁 |
|---|---|---|
| Controller | Service | ❌ Mapper / DAO(绝对不能越界) |
| Service | Mapper / DAO,其他Service,RedisTemplate | ❌ Controller(循环依赖) |
| Mapper/DAO | 无(只做数据访问) | ❌ Service(绝对不能反向依赖) |
核心原则:上层依赖下层,下层绝不依赖上层。数据只能从Controller → Service → Mapper 单向流动。
五、再加一个“隐藏层”:Config / 配置层
业务代码之外,还有一些技术支撑的配置类,比如:
RedisConfig:配置Redis序列化方式。WebMvcConfig:配置跨域、拦截器。MybatisPlusConfig:配置分页插件。
这些不属于业务分层,但它们是项目启动的“基础设施”。
用一句话总结这五层的关系:
Controller接客(收请求),Service算账(写逻辑),Mapper跑腿(查数据库),DTO搬货(传数据),ExceptionHandler擦屁股(统一处理异常)。
如果你现在正在搭建自己的项目,我建议你按照这个结构创建一个干净的包目录:
text
com.example.project ├── controller ├── service │ ├── impl ├── mapper ├── entity (PO,数据库实体) ├── dto (请求/响应对象) │ ├── request │ └── response ├── config (配置类) ├── handler (全局异常处理) └── utils (工具类)