跳转至

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 项目 | 下一章:参数接收与校验