前言
这一篇进入后端开发的核心:Spring Boot 整合 MyBatis,以及三层架构。
Spring Boot 是目前 Java 后端开发的主流框架,它把 Spring 的复杂配置简化到了极致。三层架构则是后端项目的标准组织方式,Controller 接请求、Service 写业务、Mapper 操作数据库
一、HTTP 协议状态码
1.1 什么是 HTTP 状态码?
HTTP 状态码是服务器对请求的响应结果标识,用三位数字表示。
1.2 常见状态码
| 状态码 | 含义 | 说明 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 创建成功(如新增) |
| 204 | No Content | 成功但无返回内容(如删除) |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 资源未修改(走缓存) |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证(未登录) |
| 403 | Forbidden | 已认证但无权限 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 请求方法不允许 |
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务不可用 |
1.3 状态码分类
| 范围 | 含义 |
|---|---|
| 1xx | 信息性状态码 |
| 2xx | 成功 |
| 3xx | 重定向 |
| 4xx | 客户端错误 |
| 5xx | 服务器错误 |
1.4 重要提醒
200 只表示请求成功,不表示业务成功。业务成功与否,要看响应体里的业务状态码。
json
{ "code": 200, "message": "成功", "data": { ... } }二、SpringBoot 工程搭建及 yml 配置
2.1 创建 Spring Boot 项目
用 IDEA 的 Spring Initializr 创建,或者用 start.spring.io:
Project:Maven
Language:Java
Spring Boot:3.x
Dependencies:Spring Web、MyBatis Framework、MySQL Driver、Lombok
2.2 项目结构
text
src/main/java/com/example/demo/ ├── DemoApplication.java # 启动类 ├── controller/ # 控制层 ├── service/ # 业务层 │ └── impl/ # 业务实现 ├── mapper/ # 持久层接口 ├── entity/ # 实体类 ├── dto/ # 数据传输对象 └── common/ # 公共类(Result、异常等) src/main/resources/ ├── application.yml # 配置文件 └── mapper/ # MyBatis XML
2.3 启动类
java
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }注意:启动类必须放在最外层包,这样 Spring Boot 才能扫描到所有子包。
2.4 application.yml 配置
yaml
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
三、Controller 创建及请求路径
3.1 创建 Controller
java
@RestController @RequestMapping("/users") public class UserController { @GetMapping("/{id}") public Result<User> getById(@PathVariable Integer id) { // ... } @PostMapping public Result<Void> add(@RequestBody User user) { // ... } }3.2 常用注解
| 注解 | 说明 |
|---|---|
@RestController | 等于@Controller+@ResponseBody,返回 JSON |
@RequestMapping | 类级路径前缀 |
@GetMapping | GET 请求,查询 |
@PostMapping | POST 请求,新增 |
@PutMapping | PUT 请求,修改 |
@DeleteMapping | DELETE 请求,删除 |
3.3 请求路径拼接
最终路径 = 类级路径 + 方法级路径
java
@RestController @RequestMapping("/users") // 类级 public class UserController { @GetMapping("/{id}") // 方法级 public Result<User> getById(@PathVariable Integer id) { } @PostMapping // 方法级为空 public Result<Void> add(@RequestBody User user) { } }| 方法 | 最终路径 | 请求方法 |
|---|---|---|
| getById | /users/{id} | GET |
| add | /users | POST |
3.4 四件套对应 CRUD
| 注解 | 请求方法 | 对应操作 | 示例路径 |
|---|---|---|---|
@GetMapping | GET | 查询 | /users、/users/1 |
@PostMapping | POST | 新增 | /users |
@PutMapping | PUT | 修改 | /users/1 |
@DeleteMapping | DELETE | 删除 | /users/1 |
四、常见参数绑定方式
4.1 五种常见方式
| 方式 | 注解 | 适用场景 |
|---|---|---|
| 路径参数 | @PathVariable | RESTful 风格,如/users/1 |
| 查询参数 | @RequestParam | URL 参数,如?name=张三 |
| 请求体 | @RequestBody | JSON 请求体(POST/PUT) |
| 表单参数 | 无注解 | 表单提交 |
| 请求头 | @RequestHeader | 获取请求头 |
4.2 路径参数
java
@GetMapping("/{id}") public Result<User> getById(@PathVariable Integer id) { }请求:GET /users/1
4.3 查询参数
java
@GetMapping public Result<List<User>> list( @RequestParam(required = false) String name, @RequestParam(defaultValue = "1") Integer page) { }请求:GET /users?name=张三&page=1
4.4 请求体
java
@PostMapping public Result<Void> add(@RequestBody User user) { }请求:POST /users,Body 是 JSON
4.5 表单参数
java
@PostMapping("/login") public Result<String> login(String username, String password) { }请求:POST /login,Content-Type 是application/x-www-form-urlencoded
五、响应类型封装
5.1 为什么要封装?
统一返回格式,前端好处理:
json
{ "code": 200, "message": "成功", "data": { ... } }5.2 封装 Result 类
java
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("成功"); result.setData(data); return result; } public static <T> Result<T> success() { return success(null); } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } public static <T> Result<T> error(String message) { return error(500, message); } }5.3 使用
java
@GetMapping("/{id}") public Result<User> getById(@PathVariable Integer id) { User user = userService.getById(id); return Result.success(user); } @PostMapping public Result<Void> add(@RequestBody User user) { userService.add(user); return Result.success(); }5.4 业务状态码
| code | 含义 |
|---|---|
| 200 | 成功 |
| 400 | 参数错误 |
| 401 | 未登录 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
六、Apifox 安装及测试
6.1 什么是 Apifox?
Apifox 是 API 文档和测试工具,集 API 设计、调试、Mock、测试于一体。
6.2 安装
去 apifox.com 下载安装。
6.3 测试接口步骤
新建项目
新建接口
填写请求方法、URL、参数
点击"发送"
查看响应结果
6.4 测试 POST 接口
| 配置项 | 值 |
|---|---|
| Method | POST |
| URL | http://localhost:8080/users |
| Body 类型 | raw |
| Body 格式 | JSON |
| Body 内容 | {"name": "张三", "age": 18} |
6.5 常见错误
| 错误 | 原因 |
|---|---|
| 404 | 路径不对,检查类级 + 方法级 |
| 405 | 请求方法不对,GET/POST 写错 |
| 400 | 参数格式不对 |
| 415 | Content-Type 不对,JSON 要选raw + JSON |
| 500 | 服务器内部错误,看日志 |
七、SpringBoot 整合 MyBatis 步骤
7.1 四步走
| 步骤 | 内容 |
|---|---|
| 1 | 引入依赖 |
| 2 | 配置 yml |
| 3 | 启动类加@MapperScan |
| 4 | 写 Mapper 接口和 XML |
7.2 引入依赖
xml
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
7.3 配置 yml
yaml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true
7.4 启动类加 @MapperScan
java
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { }7.5 写 Mapper
java
@Mapper public interface UserMapper { User selectById(Integer id); List<User> selectAll(); int insert(User user); }xml
<mapper namespace="com.example.demo.mapper.UserMapper"> <select id="selectById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>八、三层架构:基本结构,各层意义
8.1 什么是三层架构?
三层架构就是把后端代码分成三层:Controller、Service、Mapper。
text
请求 → Controller → Service → Mapper → 数据库 响应 ← Controller ← Service ← Mapper ← 数据库
8.2 各层职责
| 层 | 职责 | 说明 |
|---|---|---|
| Controller | 接收请求、返回响应 | 不写业务逻辑 |
| Service | 业务逻辑 | 核心业务在这里 |
| Mapper | 数据库操作 | 只负责 CRUD |
8.3 代码示例
java
// Controller:接收请求 @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public Result<User> getById(@PathVariable Integer id) { return Result.success(userService.getById(id)); } }java
// Service 接口 public interface UserService { User getById(Integer id); } // Service 实现:业务逻辑 @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User getById(Integer id) { return userMapper.selectById(id); } }java
// Mapper:数据库操作 @Mapper public interface UserMapper { User selectById(Integer id); }8.4 为什么要分层?
| 原因 | 说明 |
|---|---|
| 职责单一 | 每层只做自己的事 |
| 解耦 | 改一层不影响其他层 |
| 可复用 | Service 可以被多个 Controller 调用 |
| 易维护 | 出问题定位到具体层 |
九、三层架构:Service 层为什么需要接口
9.1 问题
Service 层为什么要定义接口UserService,再写实现类UserServiceImpl?直接写实现类不行吗?
9.2 答案:为了解耦
Controller 依赖接口,不依赖实现类。
java
// Controller 依赖接口 @Autowired private UserService userService; // 接口 // 不是这样 @Autowired private UserServiceImpl userService; // 实现类
9.3 好处
| 好处 | 说明 |
|---|---|
| 解耦 | Controller 不关心具体实现 |
| 可替换 | 换实现类不用改 Controller |
| 易测试 | 可以 Mock 接口 |
| 符合规范 | 面向接口编程 |
9.4 举例
java
// 接口 public interface UserService { User getById(Integer id); } // 实现类 A @Service public class UserServiceImplA implements UserService { public User getById(Integer id) { // 从数据库查 } } // 实现类 B @Service public class UserServiceImplB implements UserService { public User getById(Integer id) { // 从缓存查 } }Controller 只依赖UserService,换实现类不用改 Controller。
十、什么是 IoC?
10.1 什么是 IoC?
IoC(Inversion of Control,控制反转)是一种设计思想:对象的创建权和管理权交给 Spring 容器,而不是自己new。
10.2 传统方式 vs IoC
传统方式:
java
public class UserController { private UserService userService = new UserServiceImpl(); // 自己 new }IoC 方式:
java
@RestController public class UserController { @Autowired private UserService userService; // Spring 注入 }10.3 IoC 的好处
| 好处 | 说明 |
|---|---|
| 解耦 | 不用自己 new,不用关心创建细节 |
| 统一管理 | 所有 Bean 由 Spring 容器管理 |
| 方便替换 | 换实现类只需改配置 |
十一、什么是 DI?
11.1 什么是 DI?
DI(Dependency Injection,依赖注入)是 IoC 的具体实现方式:Spring 容器把依赖的对象注入到需要的地方。
11.2 三种注入方式
| 方式 | 写法 | 推荐度 |
|---|---|---|
| 字段注入 | @Autowired加在字段上 | ⭐⭐ |
| 构造器注入 | 构造方法参数 | ⭐⭐⭐⭐⭐ |
| Setter 注入 | @Autowired加在 setter 上 | ⭐ |
11.3 代码示例
java
// 字段注入 @RestController public class UserController { @Autowired private UserService userService; } // 构造器注入(推荐) @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } }11.4 IoC vs DI
| 对比项 | IoC | DI |
|---|---|---|
| 是什么 | 设计思想 | 具体实现 |
| 关注点 | 控制权转移 | 依赖如何注入 |
| 关系 | DI 是 IoC 的实现方式 |
十二、后端接口开发流程
12.1 标准流程
text
1. 分析需求 → 确定接口 2. 设计数据库表 3. 写 Entity 4. 写 Mapper(接口 + XML) 5. 写 Service(接口 + 实现) 6. 写 Controller 7. Apifox 测试
12.2 以"根据 ID 查用户"为例
1. 分析需求:GET /users/{id},返回用户信息
2. 数据库表:
sql
CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), age INT );
3. Entity:
java
@Data public class User { private Integer id; private String name; private Integer age; }4. Mapper:
java
@Mapper public interface UserMapper { User selectById(Integer id); }xml
<select id="selectById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select>5. Service:
java
public interface UserService { User getById(Integer id); } @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User getById(Integer id) { return userMapper.selectById(id); } }6. Controller:
java
@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public Result<User> getById(@PathVariable Integer id) { return Result.success(userService.getById(id)); } }7. 测试:Apifox 请求GET /users/1
十三、多条件搜索 + 分页的接口实现方案
13.1 需求
GET /users?name=张三&age=18&page=1&size=10
13.2 查询参数封装
java
@Data public class UserQuery { private String name; private Integer age; private Integer page = 1; private Integer size = 10; }13.3 Controller
java
@GetMapping public Result<PageResult<User>> list(UserQuery query) { return Result.success(userService.page(query)); }13.4 Service
java
@Override public PageResult<User> page(UserQuery query) { PageHelper.startPage(query.getPage(), query.getSize()); List<User> list = userMapper.selectByCondition(query); PageInfo<User> pageInfo = new PageInfo<>(list); PageResult<User> result = new PageResult<>(); result.setList(list); result.setTotal(pageInfo.getTotal()); return result; }13.5 Mapper XML
xml
<select id="selectByCondition" resultType="User"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="age != null"> AND age = #{age} </if> </where> </select>13.6 分页结果封装
java
@Data public class PageResult<T> { private List<T> list; private Long total; }十四、使用 @Transactional 处理事务
14.1 什么是事务?
事务是一组操作,要么全部成功,要么全部失败。
14.2 @Transactional 用法
java
@Service public class OrderServiceImpl implements OrderService { @Transactional @Override public void createOrder(Order order) { orderMapper.insert(order); // 插入订单 orderItemMapper.insertBatch(order); // 插入订单明细 stockMapper.reduce(order); // 扣减库存 // 任何一步失败,全部回滚 } }14.3 关键属性
| 属性 | 说明 | 默认值 |
|---|---|---|
rollbackFor | 哪些异常回滚 | RuntimeException |
propagation | 传播行为 | REQUIRED |
isolation | 隔离级别 | DEFAULT |
readOnly | 是否只读 | false |
14.4 常见坑
java
// ❌ 坑 1:异常被 catch 了,不会回滚 @Transactional public void test() { try { orderMapper.insert(order); int i = 1 / 0; } catch (Exception e) { e.printStackTrace(); // 异常被吃掉,事务不回滚 } } // ✅ 解决:抛出异常或手动回滚 @Transactional public void test() { try { orderMapper.insert(order); int i = 1 / 0; } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }java
// ❌ 坑 2:同类内部调用,事务失效 @Service public class OrderService { public void methodA() { methodB(); // 内部调用,@Transactional 失效 } @Transactional public void methodB() { } }十五、全局异常处理方案
15.1 为什么需要全局异常处理?
如果每个接口都 try-catch,代码冗余且难维护。全局异常处理可以统一捕获异常,统一返回格式。
15.2 实现方式
java
@RestControllerAdvice public class GlobalExceptionHandler { // 处理业务异常 @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldError().getDefaultMessage(); return Result.error(400, message); } // 处理其他异常 @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常", e); return Result.error(500, "系统繁忙,请稍后再试"); } }15.3 自定义业务异常
java
@Data public class BusinessException extends RuntimeException { private Integer code; public BusinessException(Integer code, String message) { super(message); this.code = code; } public BusinessException(String message) { this(500, message); } }15.4 使用
java
@Service public class UserServiceImpl implements UserService { @Override public User getById(Integer id) { User user = userMapper.selectById(id); if (user == null) { throw new BusinessException(404, "用户不存在"); } return user; } }十六、文件上传
16.1 Controller
java
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } String url = fileService.upload(file); return Result.success(url); }16.2 Service
java
@Service public class FileServiceImpl implements FileService { @Value("${file.upload-path}") private String uploadPath; @Override public String upload(MultipartFile file) { String originalName = file.getOriginalFilename(); String ext = originalName.substring(originalName.lastIndexOf(".")); String newName = UUID.randomUUID() + ext; File dest = new File(uploadPath + newName); try { file.transferTo(dest); } catch (IOException e) { throw new BusinessException("文件上传失败"); } return "/upload/" + newName; } }16.3 yml 配置
yaml
spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB file: upload-path: D:/upload/
十七、一个请求的完整流程链路
17.1 完整链路
text
前端请求 ↓ Nginx(反向代理) ↓ Spring Boot(Tomcat) ↓ DispatcherServlet ↓ 拦截器(Interceptor) ↓ Controller ↓ Service ↓ Mapper ↓ 数据库 ↓ 返回结果
17.2 各环节职责
| 环节 | 职责 |
|---|---|
| Nginx | 反向代理、负载均衡、静态资源 |
| Tomcat | 接收 HTTP 请求 |
| DispatcherServlet | 分发请求到 Controller |
| Interceptor | 登录校验、权限校验 |
| Controller | 接收参数、调用 Service |
| Service | 业务逻辑、事务 |
| Mapper | 数据库操作 |
| 数据库 | 存储数据 |
十八、Axios 的常用 API
18.1 什么是 Axios?
Axios 是一个基于 Promise 的 HTTP 客户端,用于前端发请求。
18.2 常用方法
| 方法 | 说明 |
|---|---|
axios.get(url, config) | GET 请求 |
axios.post(url, data, config) | POST 请求 |
axios.put(url, data, config) | PUT 请求 |
axios.delete(url, config) | DELETE 请求 |
18.3 代码示例
javascript
// GET,参数用 params axios.get('/users', { params: { page: 1, size: 10 } }) // POST,数据用 data axios.post('/users', { name: '张三', age: 18 }) // PUT axios.put('/users/1', { name: '李四' }) // DELETE,一般用路径传参 axios.delete('/users/1')18.4 参数位置
| 请求方法 | 参数位置 |
|---|---|
| GET | params |
| POST/PUT | data |
| DELETE | 路径 |
十九、Axios 的封装
19.1 为什么要封装?
统一配置 baseURL、超时、请求头、拦截器,避免每个请求都写一遍。
19.2 封装 request.js
javascript
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器 request.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截器 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '请求失败') return Promise.reject(error) } ) export default request19.3 封装 api 模块
javascript
// api/user.js import request from '@/utils/request' export function getUserList(params) { return request.get('/users', { params }) } export function addUser(data) { return request.post('/users', data) }19.4 在组件中使用
vue
<script setup> import { getUserList } from '@/api/user' async function fetchList() { const list = await getUserList({ page: 1, size: 10 }) console.log(list) } </script>二十、同源策略,跨域请求
20.1 什么是同源策略?
同源 = 协议 + 域名 + 端口,三者完全相同。
| 地址 | 是否同源 | 原因 |
|---|---|---|
http://localhost:8080vshttp://localhost:8080 | ✅ | 完全相同 |
http://localhost:8080vshttp://localhost:5173 | ❌ | 端口不同 |
http://localhost:8080vshttps://localhost:8080 | ❌ | 协议不同 |
http://localhost:8080vshttp://127.0.0.1:8080 | ❌ | 域名不同 |
20.2 什么是跨域?
浏览器出于安全考虑,限制跨域请求。前端localhost:5173请求后端localhost:8080,端口不同,就是跨域。
20.3 解决跨域:后端配置
java
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }20.4 解决跨域:前端代理
javascript
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })二十一、常见的错误调试方案
21.1 常见错误与排查
| 错误 | 可能原因 | 排查方法 |
|---|---|---|
| 404 | 路径不对 | 检查类级 + 方法级路径 |
| 405 | 请求方法不对 | 检查 GET/POST/PUT/DELETE |
| 400 | 参数格式不对 | 检查参数类型、必填项 |
| 415 | Content-Type 不对 | JSON 请求选raw + JSON |
| 500 | 服务器内部错误 | 看控制台日志、断点调试 |
| 401 | 未登录 | 检查 token 是否携带 |
| 403 | 无权限 | 检查权限配置 |
| 跨域错误 | 同源策略 | 后端配 CORS 或前端配代理 |
| 数据库连接失败 | 配置错误 | 检查 url、username、password |
| SQL 语法错误 | SQL 写错 | 看 MyBatis 日志输出的 SQL |
21.2 调试步骤
看控制台日志:异常堆栈会指出具体位置
看 MyBatis 日志:确认 SQL 和参数是否正确
断点调试:在关键位置打断点,看变量值
Apifox 单独测试:排除前端问题
看浏览器 Network:确认请求和响应