REST 与分层架构¶
后端代码不该一坨塞在 Controller 里。Spring 项目的标准姿势是分层:Controller → Service → Mapper。
三层架构¶
flowchart TD
C["Controller 接口层\n接收请求、返回响应"] -->|"调用"| S["Service 业务层\n业务逻辑、校验"]
S -->|"调用"| M["Mapper 数据层\n读写数据库"]
M --> DB[("MySQL")]
| 层 | 职责 | 对标前端 |
|---|---|---|
| Controller | 接 HTTP 请求、参数校验、调 Service、包装返回 | API 路由处理函数 |
| Service | 核心业务逻辑(算钱、校验、组合数据) | composable / 业务函数 |
| Mapper | 读写数据库 | 调用数据 API 获取数据 |
原则:Controller 保持"瘦"(只做转发),业务逻辑放 Service,数据访问放 Mapper。这样每层职责单一、好测试、好维护。
一次请求的完整流转¶
以 POST /tasks 创建任务为例:
HTTP 请求 → JwtInterceptor(校验token) → TaskController.create()
→ TaskService.createTask() (业务:组装数据)
→ TaskServiceImpl.save() (MyBatis-Plus 自动 insert)
→ MySQL
→ 返回 Result<Task> → 转 JSON → HTTP 响应
实际代码(四层全貌)¶
Controller 层——接请求、调 Service:
package com.javaglm.task.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.javaglm.task.common.Result;
import com.javaglm.task.dto.TaskRequest;
import com.javaglm.task.entity.Task;
import com.javaglm.task.service.TaskService;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
/**
* 任务接口(第 28 章):增删改查 + 分页。
* 注意 userId 来自 JWT 拦截器放进请求上下文的值(不是前端传的,防伪造)。
*/
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping
public Result<IPage<Task>> list(
@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "10") long size,
@RequestParam(required = false) Integer status,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
return Result.success(taskService.pageTasks(current, size, userId, status));
}
@PostMapping
public Result<Task> create(@RequestBody @Valid TaskRequest req,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
return Result.success(taskService.createTask(req, userId));
}
@PutMapping("/{id}")
public Result<Task> update(@PathVariable Long id,
@RequestBody @Valid TaskRequest req,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
return Result.success(taskService.updateTask(id, req, userId));
}
@DeleteMapping("/{id}")
public Result<?> delete(@PathVariable Long id, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
taskService.deleteTask(id, userId);
return Result.success();
}
}
Service 接口——定义业务契约:
package com.javaglm.task.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.javaglm.task.dto.TaskRequest;
import com.javaglm.task.entity.Task;
/**
* 任务服务接口(第 28 章)。
* 继承 IService<Task>,白嫖 MyBatis-Plus 的 save/getById/updateById/removeById 等方法,
* 只需声明本系统特有的业务方法。
*/
public interface TaskService extends IService<Task> {
/** 分页查询某用户的任务,可按状态过滤。 */
IPage<Task> pageTasks(long current, long size, Long userId, Integer status);
/** 新建任务。 */
Task createTask(TaskRequest req, Long userId);
/** 修改任务(校验归属)。 */
Task updateTask(Long id, TaskRequest req, Long userId);
/** 删除任务(逻辑删除,校验归属)。 */
void deleteTask(Long id, Long userId);
}
Service 实现——真正的业务逻辑:
package com.javaglm.task.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.javaglm.task.common.BizException;
import com.javaglm.task.dto.TaskRequest;
import com.javaglm.task.entity.Task;
import com.javaglm.task.mapper.TaskMapper;
import com.javaglm.task.service.TaskService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* 任务服务实现(第 28 章)。
* 继承 ServiceImpl<TaskMapper, Task>:自动拥有 CRUD,Mapper 也自动注入。
* 业务逻辑写在这里(校验归属、组装数据),Controller 保持"瘦"。
*/
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements TaskService {
@Override
public IPage<Task> pageTasks(long current, long size, Long userId, Integer status) {
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Task::getUserId, userId);
if (status != null) {
wrapper.eq(Task::getStatus, status);
}
wrapper.orderByDesc(Task::getCreateTime);
return this.page(new Page<>(current, size), wrapper);
}
@Override
public Task createTask(TaskRequest req, Long userId) {
Task task = new Task();
task.setTitle(req.getTitle());
task.setDescription(req.getDescription());
task.setStatus(req.getStatus() == null ? 0 : req.getStatus());
task.setUserId(userId);
task.setCreateTime(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
this.save(task);
return task;
}
@Override
public Task updateTask(Long id, TaskRequest req, Long userId) {
Task task = this.getById(id);
if (task == null || !task.getUserId().equals(userId)) {
throw new BizException(404, "任务不存在或无权操作");
}
task.setTitle(req.getTitle());
task.setDescription(req.getDescription());
if (req.getStatus() != null) {
task.setStatus(req.getStatus());
}
task.setUpdateTime(LocalDateTime.now());
this.updateById(task);
return task;
}
@Override
public void deleteTask(Long id, Long userId) {
Task task = this.getById(id);
if (task == null || !task.getUserId().equals(userId)) {
throw new BizException(404, "任务不存在或无权操作");
}
this.removeById(id); // @TableLogic:实际执行逻辑删除
}
}
Mapper——数据访问(继承 BaseMapper,自动有 CRUD):
package com.javaglm.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.javaglm.task.entity.Task;
/**
* 任务 Mapper(第 26 章)。
* 继承 BaseMapper<Task>,白嫖一堆 CRUD 方法(insert/updateById/deleteById/selectById/selectList...),
* 一行 SQL 都不用写。需要自定义 SQL 时再在这里加方法 + 写 XML/注解。
*/
public interface TaskMapper extends BaseMapper<Task> {
}
关键注解速记¶
| 注解 | 作用 |
|---|---|
@RestController |
标记 Controller,返回值转 JSON |
@RequestMapping("/tasks") |
类级别路径前缀 |
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping |
对应 HTTP 方法 |
@Service |
标记 Service 实现类,被 Spring 管理 |
@Autowired(或构造器) |
注入依赖 |
为什么 Service 要拆接口 + 实现
接口 TaskService 定义"做什么",实现 TaskServiceImpl 定义"怎么做"。Controller 依赖接口(第 09 章的面向接口编程),以后换实现、写测试 mock,都不用动 Controller。这是工程规范,不是必须,但推荐。
:octicons-arrow-left-16: 上一章:第一个 Spring Boot 项目 | 下一章:参数接收与校验