【Spring Boot + MyBatis|第1篇】三层架构详解:Controller、Service、Mapper 到底分别做什么?
前言
在前面写项目的时候,我们经常会看到一个非常固定的结构:
- Controller 层
- Service 层
- Mapper 层
刚开始学习 Spring Boot + MyBatis 的时候,很多人会觉得这个结构有点麻烦。明明一个查询功能,Controller 里面直接查数据库不就行了吗?为什么还要经过 Service,再经过 Mapper?
其实三层架构不是为了把代码写复杂,而是为了让每一层只负责自己的事情。
这一篇我们就来单独梳理一下 Spring Boot 项目中最常见的三层架构,看看 Controller、Service、Mapper 分别负责什么,以及一个请求从前端到数据库到底是怎么流转的。
一、三层架构是什么?
在 Spring Boot + MyBatis 项目中,常见的后端结构一般是这样的:
controller 接收前端请求,返回响应结果
service 处理业务逻辑
mapper 操作数据库
如果用一句话来理解:
Controller 管请求,Service 管业务,Mapper 管数据库。
比如我们要实现一个“根据 id 查询员工信息”的功能,整体流程就是:
前端发送请求
↓
Controller 接收请求
↓
Service 处理业务
↓
Mapper 查询数据库
↓
数据库返回数据
↓
Service 返回结果
↓
Controller 响应前端
这样拆分之后,每一层的职责就比较清楚了。
二、准备一个简单案例
这里我们用“员工查询”作为例子,假设项目中有一张员工表 emp,我们要根据员工 id 查询员工信息。
1. 实体类 Emp
public class Emp {
private Integer id;
private String username;
private String name;
private Integer gender;
private String phone;
// getter、setter 省略
}
实体类主要用来封装数据库中的一条员工数据。
比如数据库中有一条员工记录,查询出来之后,就可以封装成一个 Emp 对象返回。
2. 统一返回结果 Result
public class Result {
private Integer code;
private String msg;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.code = 1;
result.msg = "success";
result.data = data;
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.code = 0;
result.msg = msg;
result.data = null;
return result;
}
}
在项目开发中,我们一般不会直接把对象返回给前端,而是使用统一格式返回。
这样前端拿到的数据结构就比较固定,例如:
{
"code": 1,
"msg": "success",
"data": {
"id": 1,
"username": "zhangsan",
"name": "张三"
}
}
三、Controller 层:接收请求,响应结果
1. Controller 代码
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpService empService;
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Emp emp = empService.getById(id);
return Result.success(emp);
}
}
2. 文字说明
Controller 层主要负责和前端打交道。
在这个接口中:
@GetMapping("/{id}")
表示当前接口接收一个 GET 请求,请求路径类似:
/emps/1
这里的 1 就是员工 id。
@PathVariable Integer id
表示从请求路径中获取 id 参数。
然后 Controller 并没有直接查询数据库,而是调用了:
empService.getById(id)
也就是说,Controller 只负责接收请求和返回结果,不负责具体业务,也不直接写 SQL。
3. 涉及知识点
1. @RestController
@RestController 表示当前类是一个控制器,并且方法返回的数据会直接转换成 JSON 响应给前端。
它相当于:
@Controller
@ResponseBody
2. @RequestMapping
@RequestMapping("/emps") 用来设置当前 Controller 的公共请求路径。
如果类上是 /emps,方法上是 /{id},那么完整路径就是:
/emps/{id}
3. @PathVariable
@PathVariable 用来接收路径参数。
比如请求路径是:
/emps/10
那么 id 的值就是 10。
四、Service 层:处理业务逻辑
1. Service 接口
public interface EmpService {
Emp getById(Integer id);
}
2. Service 实现类
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public Emp getById(Integer id) {
return empMapper.getById(id);
}
}
3. 文字说明
Service 层主要负责业务逻辑。
在这个简单查询案例中,业务逻辑比较少,所以 Service 层只是调用 Mapper 查询数据。
但是在真实项目中,Service 层会非常重要。比如:
- 判断参数是否合法
- 判断数据是否存在
- 处理新增、修改、删除的业务规则
- 调用多个 Mapper 完成复杂操作
- 控制事务
- 处理登录、权限、状态校验等逻辑
所以不能因为当前案例简单,就觉得 Service 层没有意义。
比如以后我们查询员工时,可能要加一个判断:
@Override
public Emp getById(Integer id) {
if (id == null || id <= 0) {
throw new RuntimeException("员工id不合法");
}
return empMapper.getById(id);
}
这种判断就应该放在 Service 层,而不是放在 Mapper 层。
4. 涉及知识点
1. @Service
@Service 表示当前类是业务层组件,会交给 Spring 容器管理。
加上这个注解之后,Controller 中才能通过 @Autowired 注入它。
2. 接口和实现类分离
在项目中,Service 层经常会写成:
EmpService
EmpServiceImpl
这样做的好处是结构更清晰,也方便后期扩展。
Controller 只依赖接口,不需要关心具体实现类怎么写。
五、Mapper 层:操作数据库
1. Mapper 接口
@Mapper
public interface EmpMapper {
@Select("select id, username, name, gender, phone from emp where id = #{id}")
Emp getById(Integer id);
}
2. 文字说明
Mapper 层主要负责和数据库打交道。
在这个方法中:
@Select("select id, username, name, gender, phone from emp where id = #{id}")
表示执行一条查询 SQL。
其中:
#{id}
表示把方法参数 id 传入 SQL 中。
如果调用:
empMapper.getById(1)
最终执行的 SQL 大概就是:
select id, username, name, gender, phone from emp where id = 1;
Mapper 层只负责数据库操作,不应该写复杂业务判断。
3. 涉及知识点
1. @Mapper
@Mapper 表示当前接口是 MyBatis 的 Mapper 接口。
Spring Boot 启动时会扫描到这个接口,并为它创建代理对象。
所以我们不需要自己写实现类,MyBatis 会帮我们完成。
2. #{}
#{} 是 MyBatis 中的参数占位符。
它可以防止 SQL 注入,开发中应该优先使用 #{},不要随便使用 ${}。
六、为什么不直接在 Controller 里写 SQL?
有些初学者可能会想,既然最后还是查数据库,那我直接在 Controller 里调用 Mapper 不就行了吗?
比如这样:
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpMapper empMapper;
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Emp emp = empMapper.getById(id);
return Result.success(emp);
}
}
这样写在功能上确实可以运行,但是不推荐。
原因很简单:Controller 的职责会变得混乱。
如果所有逻辑都写在 Controller 中,后期代码可能会变成这样:
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
if (id == null || id <= 0) {
return Result.error("id不合法");
}
Emp emp = empMapper.getById(id);
if (emp == null) {
return Result.error("员工不存在");
}
// 以后可能还有权限判断、状态判断、日志记录等逻辑
return Result.success(emp);
}
这样 Controller 就不只是接收请求了,它还处理参数校验、业务判断、数据库查询,代码会越来越乱。
所以更推荐的写法是:
Controller:接收请求,返回响应
Service:处理业务逻辑
Mapper:执行数据库操作
各做各的事情,后期维护起来才更清楚。
七、完整调用流程梳理
我们再把整个流程串起来。
当前端请求:
GET /emps/1
首先进入 Controller:
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Emp emp = empService.getById(id);
return Result.success(emp);
}
Controller 调用 Service:
Emp emp = empService.getById(id);
Service 调用 Mapper:
return empMapper.getById(id);
Mapper 执行 SQL:
@Select("select id, username, name, gender, phone from emp where id = #{id}")
Emp getById(Integer id);
数据库返回数据后,再一层一层返回:
Mapper -> Service -> Controller -> 前端
这就是一个最基础的三层架构调用流程。
八、三层架构每一层应该写什么?
可以简单总结成下面这样:
| 层级 | 主要职责 | 常见内容 |
|---|---|---|
| Controller | 接收请求,响应结果 | 请求路径、参数接收、返回 Result |
| Service | 处理业务逻辑 | 参数校验、业务判断、事务控制 |
| Mapper | 操作数据库 | SQL 查询、插入、修改、删除 |
开发时可以记住一句话:
Controller 不写业务,Service 不写 SQL,Mapper 不管业务。
当然这句话不是绝对的,但是对于初学阶段非常有帮助。
九、常见问题总结
1. Controller 能不能直接调用 Mapper?
技术上可以,但是不推荐。
因为这样会跳过 Service 层,导致业务逻辑容易堆在 Controller 中,后期维护困难。
2. Service 层是不是每次都必须写?
在标准项目中建议写。
即使当前只是简单查询,也建议保留 Service 层。因为项目后期很可能会增加业务逻辑。
3. Mapper 接口为什么不用写实现类?
因为 MyBatis 会根据 Mapper 接口和 SQL 自动创建代理对象。
我们只需要定义接口方法和 SQL,具体执行过程由 MyBatis 完成。
4. 实体类、DTO、VO 和三层架构是什么关系?
实体类一般对应数据库表。
DTO 通常用于接收前端参数。
VO 通常用于返回前端展示数据。
三层架构关注的是代码分层,而实体类、DTO、VO 关注的是数据传输对象的设计。
十、总结
这一篇主要学习了 Spring Boot + MyBatis 项目中最常见的三层架构。
Controller 层负责接收前端请求,并返回统一结果;Service 层负责处理业务逻辑;Mapper 层负责操作数据库。
刚开始写项目时,三层架构可能看起来有点繁琐,但是当功能越来越多时,它的好处就会越来越明显。它可以让代码职责更清楚,也能让后期维护和扩展更加方便。
后面我们继续学习 Spring Boot 项目中常见的参数接收方式,也就是 @RequestParam、@PathVariable、@RequestBody 的区别。
更多推荐



所有评论(0)