ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + MyBatis 整合与三层架构

Spring Boot + MyBatis 整合与三层架构

前言

这一篇进入后端开发的核心:Spring Boot 整合 MyBatis,以及三层架构。

Spring Boot 是目前 Java 后端开发的主流框架,它把 Spring 的复杂配置简化到了极致。三层架构则是后端项目的标准组织方式,Controller 接请求、Service 写业务、Mapper 操作数据库

一、HTTP 协议状态码

1.1 什么是 HTTP 状态码?

HTTP 状态码是服务器对请求的响应结果标识,用三位数字表示。

1.2 常见状态码

状态码含义说明
200OK请求成功
201Created创建成功(如新增)
204No Content成功但无返回内容(如删除)
301Moved Permanently永久重定向
302Found临时重定向
304Not Modified资源未修改(走缓存)
400Bad Request请求参数错误
401Unauthorized未认证(未登录)
403Forbidden已认证但无权限
404Not Found资源不存在
405Method Not Allowed请求方法不允许
500Internal Server Error服务器内部错误
502Bad Gateway网关错误
503Service 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类级路径前缀
@GetMappingGET 请求,查询
@PostMappingPOST 请求,新增
@PutMappingPUT 请求,修改
@DeleteMappingDELETE 请求,删除

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/usersPOST

3.4 四件套对应 CRUD

注解请求方法对应操作示例路径
@GetMappingGET查询/users、/users/1
@PostMappingPOST新增/users
@PutMappingPUT修改/users/1
@DeleteMappingDELETE删除/users/1

四、常见参数绑定方式

4.1 五种常见方式

方式注解适用场景
路径参数@PathVariableRESTful 风格,如/users/1
查询参数@RequestParamURL 参数,如?name=张三
请求体@RequestBodyJSON 请求体(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 测试接口步骤

  1. 新建项目

  2. 新建接口

  3. 填写请求方法、URL、参数

  4. 点击"发送"

  5. 查看响应结果

6.4 测试 POST 接口

配置项值
MethodPOST
URLhttp://localhost:8080/users
Body 类型raw
Body 格式JSON
Body 内容{"name": "张三", "age": 18}

6.5 常见错误

错误原因
404路径不对,检查类级 + 方法级
405请求方法不对,GET/POST 写错
400参数格式不对
415Content-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

对比项IoCDI
是什么设计思想具体实现
关注点控制权转移依赖如何注入
关系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 参数位置

请求方法参数位置
GETparams
POST/PUTdata
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 request

19.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参数格式不对检查参数类型、必填项
415Content-Type 不对JSON 请求选raw + JSON
500服务器内部错误看控制台日志、断点调试
401未登录检查 token 是否携带
403无权限检查权限配置
跨域错误同源策略后端配 CORS 或前端配代理
数据库连接失败配置错误检查 url、username、password
SQL 语法错误SQL 写错看 MyBatis 日志输出的 SQL

21.2 调试步骤

  1. 看控制台日志:异常堆栈会指出具体位置

  2. 看 MyBatis 日志:确认 SQL 和参数是否正确

  3. 断点调试:在关键位置打断点,看变量值

  4. Apifox 单独测试:排除前端问题

  5. 看浏览器 Network:确认请求和响应


RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进