Spring 全家桶零基础图文教程
从「这是什么」到「亲手跑通一个完整项目」—— Spring · Spring MVC · MyBatis · Spring Boot 一次学透
第 0 章课程导学:认识四件套
先搞清楚四个名词的关系,再搭好环境。磨刀不误砍柴工。
0.1 这门课适合谁?
你只需要具备两个前提:
- ✅ Java 基础:最好见过类、方法、对象这几个词(不需要熟练——忘得大半没关系,第 1 章就是专门为你准备的快速回顾);
- ✅ SQL 基础:见过
SELECT / INSERT / UPDATE / DELETE这几个词。
不需要会的:反射、设计模式、任何框架——用到时我会解释;注解会在第 1 章提前混个眼熟。
学完你将能够:独立搭建并开发一个提供 REST 接口、读写 MySQL 数据库的完整 Java 后端项目——这正是企业里「Spring 技术栈开发岗」的基本功。
0.2 一张图看懂四个技术的关系
初学者最懵的就是:Spring、Spring MVC、MyBatis、Spring Boot 到底什么关系?记住一句话:
Spring 是地基(管对象);Spring MVC 是前厅(管请求);MyBatis 是仓库通道(管数据库);Spring Boot 是总装车间(把所有东西自动装好,让你拎包入住)。
图 0-1 · 四件套在项目中的位置:Spring 横贯中三层,Boot 包裹全局,MVC 与 MyBatis 各守一头
也可以用「餐厅」来打比方:
| 技术 | 餐厅里的角色 | 干的活 |
|---|---|---|
| Spring | 店长 | 统一招聘、管理员工(对象),谁缺人手就找店长要 |
| Spring MVC | 前台服务员 | 接待客人(浏览器请求),把点单递给后厨 |
| MyBatis | 采购员 | 往返仓库(数据库)取食材、存食材 |
| Spring Boot | 加盟总部 | 装修、设备、流程提前配好,老板直接开业 |
0.3 术语速查表(先混个眼熟)
| 名词 | 人话解释 |
|---|---|
| 框架(Framework) | 别人写好的「半成品程序」,把重复劳动都做完,你只填业务逻辑。用了框架 = 站在巨人肩膀上 |
| 注解(@Xxx) | 贴在类/方法上的「标签」,框架看到标签就知道该干什么。如 @Service 表示「这是个业务类,请管理它」 |
| 依赖(Dependency) | 你项目用到的别人写的库(jar 包)。比如连 MySQL 需要 MySQL 驱动 |
| Maven / pom.xml | Java 的「依赖管家」:在 pom.xml 写一行坐标,自动下载并管理所有 jar 包及版本 |
| Bean | 被 Spring 容器创建并保管的对象。你可以理解为「有编制的员工」 |
| IoC 容器 | Spring 内部存放、管理 Bean 的「大仓库」 |
| ORM | 对象关系映射:Java 对象 ↔ 数据库表记录的自动转换技术。MyBatis 属于半自动 ORM |
| RESTful | 一种接口设计风格:URL 用名词、HTTP 动词表操作(GET 查、POST 增、PUT 改、DELETE 删) |
| JSON | 前后端之间传数据的主流文本格式,形如 {"name":"tom","age":18} |
| YAML / yml | 用缩进表示层级的配置文件格式,Spring Boot 的标配(application.yml) |
0.4 环境搭建(一次性工作,约 30 分钟)
| 工具 | 版本 | 获取方式 |
|---|---|---|
| JDK | 17(LTS)或更高 | Adoptium 官网下载 Temurin 17,安装时勾选「添加到 PATH」 |
| IntelliJ IDEA | Community 免费版即可 | jetbrains.com 下载,开发 Java 的主流编辑器 |
| MySQL | 8.x | 官网或 phpstudy 等集成包;安装时牢记 root 密码 |
| Maven | IDEA 自带 | 不用单独安装(国内必须换阿里云镜像,否则下载依赖极慢,配置见下方「常见报错表」) |
| Postman / curl | 任意 | 测试接口用;浏览器只能发 GET 请求,学 POST/PUT/DELETE 需要它 |
版本时效提醒(2026-09):Spring Boot 3.5 及更早版本的开源维护已于 2026 年 6 月全部结束,不再有免费安全补丁,新项目直接用 4.x。本教程示例基于 4.1.x,你用 Spring Initializr 生成的一般会更新,以生成结果为准。
装好后打开终端(Windows 用 PowerShell / CMD)验证:
java -version # 期望输出: openjdk version "17.x.x" 或更高
mysql --version # 期望输出: mysql Ver 8.4.xx(8.4 是当前 LTS;8.0 亦可)
两条命令都能出版本号,环境就绪。以后每次写完代码,都是「IDEA 里点运行」+「Postman 里发请求」的循环,不再碰命令行也没关系。
✍️ 自测:Spring Boot 和 Spring MVC 是什么关系?
答:Spring MVC 是 Spring 官方家族里的 Web 模块(负责 HTTP 请求处理),Spring Boot 是搭建在 Spring 之上的快速开发脚手架。用 Spring Boot 时默认就集成了 Spring MVC——所以第 4 章学的注解,在第 7 章的 Boot 项目里直接就能用。
第 1 章Java 基础快速回顾(衔接章)
Java 忘了大半?没关系——本章只回顾本课程会用到的部分,半小时捡回来。每个知识点都标注了「它会在后续哪一章再次出现」,学起来有目标。Java 还热乎的读者可直接跳去第 2 章。
1.1 类与 main 方法:一切代码的起点
Java 的所有代码都住在「类」里,程序从 main 方法开始执行:
public class Hello {
public static void main(String[] args) { // 程序入口(固定写法,先背下来)
System.out.println("Hello, Java!"); // 向控制台打印一行
}
}
三件事记住:① 每个源文件叫 类名.java;② 语句以分号 ; 结尾;③ 代码块用大括号 { } 包住。在 IDEA 新建 Java 项目、粘贴、点绿色三角就能运行。
main 方法——只不过那里面写的是「启动整个 Web 网站」。1.2 变量与常用类型
int age = 18; // 整数
long id = 100000000L; // 长整数(数据库主键常用,结尾加 L)
double price = 9.9; // 小数
boolean vip = true; // 真 / 假
String name = "tom"; // 字符串(注意大写 S,它是个对象)
var message = "你好"; // var:让编译器自己推断类型(JDK 10+)
字符串的常用操作:拼接用 +;取长度 name.length();判空 name.isBlank()。比较字符串内容必须用 name.equals("tom"),不要用 ==(== 比较的是「是否同一个对象」而不是「内容是否相同」,这是新手经典错误)。
1.3 方法:把逻辑装进可复用的盒子
public class MathUtil {
// 访问修饰符 返回值类型 方法名(参数类型 参数名)
public static int add(int a, int b) {
return a + b; // return:交回结果,方法结束
}
public static void greet(String name) { // void:不返回任何东西
System.out.println("你好, " + name);
}
}
int sum = MathUtil.add(1, 2); // 调用:类名.方法名(参数)
第 7 章实战里 Service 层的每个方法,都是这个套路,只是「盒子」里装的是业务逻辑。
1.4 类与对象:图纸与房子
类(class)是图纸,对象(object)是按图纸造出来的房子。类定义「有哪些字段(属性)和哪些行为(方法)」,new 一次就造出一个对象:
图 1-1 · 类是图纸,对象是房子:一张图纸可以造无数房子,每栋房子的数据各自独立
public class User {
private Long id; // 字段(private:只有本类能直接访问)
private String username;
// 构造方法:方法名 = 类名,new 的时候执行
public User(Long id, String username) {
this.id = id; // this.id 是字段,id 是参数
this.username = username;
}
// getter/setter:外界读取/修改私有字段的入口
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
}
User u = new User(1L, "tom"); // 创建对象
String name = u.getUsername(); // 调用对象的方法
1.5 包(package):代码的文件夹
package com.example.demo.entity; // 必须是文件第一行:声明"我住在哪个包"
import java.util.List; // 用别的包里的类,需要 import 引入
包和文件夹一一对应:com.example.demo.entity.User 对应 com/example/demo/entity/User.java。行业惯例用「域名倒写」开头(如 com.example),保证全球唯一。
com.example.demo,你的代码就必须住在它的子包里(...demo.controller 等)——第 3 章和第 6 章会反复撞见这条规则。1.6 接口与实现类:只认约定,不认品牌
接口(interface)只声明「能做什么」,实现类(implements)回答「具体怎么做」。这是 Spring 全部魔法的地基:
图 1-2 · 接口是「插座面板」(约定),实现类是「不同品牌的插头」(做法)——用插座的人不关心插头是谁造的
public interface Notifier {
void send(String message); // 接口方法没有方法体,分号结尾
}
public class PhoneNotifier implements Notifier {
@Override // 标记"我在实现/重写",写错方法名编译器会报错
public void send(String message) {
System.out.println("发送短信: " + message);
}
}
Notifier n = new PhoneNotifier(); // 关键!用接口类型接收实现对象
n.send("订单已创建"); // 调的是 PhoneNotifier 的实现
1.7 继承与多态(了解即可)
public class Animal {
public void eat() { System.out.println("吃东西"); }
}
public class Dog extends Animal { // Dog 是一种 Animal(is-a 关系)
@Override
public void eat() { System.out.println("狗啃骨头"); } // 重写父类方法
}
Animal a = new Dog();
a.eat(); // 打印"狗啃骨头"——看实际类型,不看变量类型,这就是多态
Spring 里「声明接口、运行时换实现」本质上就是多态在框架层面的应用。
1.8 集合:List 与 Map
List<String> names = new ArrayList<>(); // 列表:按顺序存放
names.add("tom"); // 添加
names.add("jerry");
names.get(0); // 按下标取(从 0 开始)
names.size(); // 元素个数
for (String name : names) { // 增强 for 遍历
System.out.println(name);
}
Map<Long, String> userNames = new HashMap<>(); // 键值对:按"键"存取
userNames.put(1L, "tom");
userNames.get(1L); // "tom"
List<User>。1.9 泛型:尖括号里的类型约束
<User> 的意思是「这个列表只装 User」,装错类型编译期直接报错,把错误拦在运行之前:
List<User> users = new ArrayList<>();
users.add("tom"); // ❌ 编译报错:只能装 User
// 泛型参数 T 是"占位符",用的时候才确定:
Result<User> r1 = Result.ok(user); // data 是单个用户
Result<List<User>> r2 = Result.ok(users); // data 是用户列表
Result<T> 的尖括号就是泛型——同一个 Result 类,既能包 User 也能包 List。1.10 注解:贴给框架看的标签
你已经见过两次 @Override 了。注解(@Xxx)本身不执行任何逻辑,它是贴在类/方法/字段上的标签——框架在运行期读取标签,然后替你干活:
@Override // 内置注解:帮编译器检查"确实重写了父类方法"
public String toString() { return "User"; }
@Service // Spring 的注解(第 3 章起):贴上它,Spring 就接管这个类
public class UserService { }
1.11 异常:出错的正确姿势
try {
int n = Integer.parseInt("abc"); // 这行会出错
} catch (NumberFormatException e) { // 捕获并处理
System.out.println("不是合法数字: " + e.getMessage());
} finally {
System.out.println("无论成败都执行"); // 常用于关闭资源
}
public User getById(Long id) {
if (id == null) {
throw new RuntimeException("id 不能为空"); // 主动抛出异常
}
// ...
}
两类异常记住一句话就够:RuntimeException(运行时异常)可以不强制处理,一路往上抛——第 4 章的全局异常处理就是「在最高处统一接住它们,转成友好提示」。
1.12 Lambda 与 Stream(能看懂即可)
// Lambda:一句话的匿名方法,常作为参数传递
names.forEach(name -> System.out.println(name));
// Stream:链式数据处理(过滤 + 收集)
List<User> adults = users.stream()
.filter(u -> u.getAge() >= 18) // 只留成年用户
.collect(Collectors.toList());
现代 Java 代码和面试都常见这套写法,看懂即可,本教程代码刻意没用它。
1.13 Java 知识 → 后续章节 对照表
| Java 知识 | 在哪里再次出现 |
|---|---|
| 类与对象 | 第 7 章实体类 User(与数据库表一一对应) |
| 方法 | 第 7 章 Service 层(一个方法一段业务) |
| 包与 import | 第 3、6 章 包扫描规则(启动类必须在上层) |
| 接口与多态 | 第 3 章 依赖注入(声明接口,容器塞实现) |
| List 与泛型 | 第 5 章查询返回 List<User>;第 7 章 Result<T> |
| 注解 | 第 3 章起无处不在(@Service、@Mapper、@GetMapping…) |
| 异常 | 第 4 章全局异常处理(统一接住 RuntimeException) |
| Lambda / Stream | 阅读他人代码、面试必备 |
✍️ 自测:接口和实现类是什么关系?为什么「用接口类型接收实现对象」很有用?
答:接口声明能力(做什么),实现类提供具体做法(怎么做)。用接口类型接收实现对象后,代码依赖的是「约定」而非「具体品牌」——以后换实现(短信换邮件),调用代码一行不用改。这正是第 3 章 Spring 依赖注入的解耦原理。
✍️ 自测:为什么字符串比较必须用 equals,不能用 ==?
答:== 比较的是两个变量是否指向同一个对象(内存地址),equals 比较的是内容。两个内容相同的字符串可能是不同对象,用 == 会误判为「不相等」。
✍️ 自测:package 声明和磁盘上的文件夹是什么关系?
答:一一对应。package com.example.demo.entity 意味着这个文件必须放在 com/example/demo/entity/ 目录下。所以「Spring 只扫描启动类所在包及子包」这条规则,落到磁盘上就是「必须放在启动类所在文件夹及其子文件夹里」。
第 2 章为什么需要框架?
理解框架最好的方式:先看看没有框架时,Java 程序员是怎么「受苦」的。
2.1 痛苦一:对象靠手 new,改一处动全身
写一个电商下单功能,最直觉的写法是「要用谁就 new 谁」:
public class OrderService {
private UserService userService = new UserService(); // 自己 new
public void createOrder() {
userService.checkUser();
}
}
public class UserService {
private UserDao userDao = new UserDao(); // 又自己 new
}
public class UserDao {
private DbUtil dbUtil = new DbUtil(); // 还是自己 new
}
小项目没问题,项目一大就会撞上三堵墙:
- 牵一发动全身——
UserDao的构造方法加个参数,全项目所有new UserDao()的地方都得改; - 资源浪费——每个对象都是新的,无法复用。数据库连接池这种「全局只要一份」的东西,自己写单例模式非常费劲;
- 没法测试——想测
OrderService,它内部写死了new UserService(),想换成假对象(Mock)都插不进手。
根源在于:每个类都亲手创建自己的依赖,类和类焊死在一起,这叫「强耦合」。
2.2 痛苦二:手写 Servlet,接个表单能写到怀疑人生
Java 最原始的 Web 开发方式叫 Servlet。接收一个注册表单:
public class UserServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 手动一个个取参数
String username = req.getParameter("username");
String ageStr = req.getParameter("age");
// 2. 手动转类型
int age = Integer.parseInt(ageStr);
// 3. 手动组装对象
User user = new User(username, age);
// 4. 手动把对象变 JSON 写回响应(还要引库、拼字符串)
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write("{\"username\":\"" + user.getUsername() + "\"}");
}
}
这还没完——还要去 web.xml 里注册这个 Servlet、配置 URL 映射。表单一大(注册 20 个字段很常见),上面的「取参数 → 转类型 → 组对象」循环要复制粘贴 20 遍。而你真正想写的业务逻辑,可能就一行。
2.3 痛苦三:手写 JDBC,查一张表 10 行起步
Java 原生操作数据库的 API 叫 JDBC:
public User findById(Long id) throws SQLException {
Connection conn = null; PreparedStatement ps = null; ResultSet rs = null;
try {
// 1. 获取连接 2. 写 SQL 3. 填参数
conn = DriverManager.getConnection(url, user, password);
ps = conn.prepareStatement("SELECT id, username FROM users WHERE id = ?");
ps.setLong(1, id);
rs = ps.executeQuery();
// 4. 手动把每列数据取出来塞进对象
User u = null;
if (rs.next()) {
u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
}
return u;
} finally {
// 5. 手动关闭资源(顺序还不能错,忘了关就泄漏连接)
rs.close(); ps.close(); conn.close();
}
}
真正有信息量的只有那一句 SQL,其余全是模板代码。表有 20 个字段,就得写 20 行 rs.getXxx + setXxx。SQL 写错只能等运行时爆错。
2.4 四个框架 = 四副解药
| 痛点 | 解药 | 一句话原理 |
|---|---|---|
| 对象乱 new、强耦合 | Spring | 对象交给容器统一创建、保管、复用,谁需要谁伸手 |
| Servlet 取参/转类型/拼 JSON 啰嗦 | Spring MVC | 方法参数直接接收请求数据,返回对象自动变 JSON |
| JDBC 样板代码堆成山 | MyBatis | 你只写 SQL,连接/参数/结果映射/关资源全自动 |
| 三者整合要写几十个 XML 配置 | Spring Boot | 自动配置 + 内嵌服务器,一个 main 方法启动全项目 |
✍️ 自测:Spring 解决的是下面哪个问题?A. 界面好看 B. 对象管理 C. SQL 优化 D. 网速快
答:B。Spring 的核心是 IoC 容器——统一创建和管理对象。SQL 由 MyBatis 负责,Web 请求由 Spring MVC 负责。
第 3 章Spring:对象管家(IoC 与 DI)
Spring 的全部核心就两个词:IoC(控制反转)与 DI(依赖注入)。唬人的名词,朴素的道理。
3.1 IoC:把「new 对象」的权力交出去
IoC(Inversion of Control,控制反转):对象的创建权从「你手里」反转到「Spring 手里」。
- 以前:
UserService userService = new UserService();—— 你自己造人; - 现在:Spring 启动时把对象创建好,放进一个「大仓库」(IoC 容器),你要用就伸手要。
容器里这些被管理的对象有个专有名词:Bean。
图 3-1 · 控制反转前后对比:创建权从各个类手里「反转」到容器手里
3.2 把类登记进容器:四个「身份注解」
怎么告诉 Spring「这个类请帮我管理」?在类上贴注解即可:
@Component // 通用组件:凡是想被管理的类都可以贴它
public class PhoneNotifier { /* ... */ }
@Service // 业务层专用(语义更清晰,效果同 Component)
public class OrderService { /* ... */ }
@Repository // 数据层专用
public class UserDao { /* ... */ }
@Controller // Web 层专用(下一章的主角)
public class UserController { /* ... */ }
| 注解 | 用在哪个层 | 说明 |
|---|---|---|
@Component | 任意 | 通用「零件」,不属于下面三层的工具类用它 |
@Service | 业务层 | 本质就是 @Component 的别名,团队一看注解就知道类的作用 |
@Repository | 数据层 | 额外会把数据库异常转换为统一异常 |
@Controller | Web 层 | 配合 @ResponseBody 使用(第 3 章) |
com.example.demo,而你的类写在 com.myapp 下,Spring 看都看不见。所以启动类永远放在所有代码的最上层包。3.3 DI:需要什么,Spring 自动塞给你
DI(Dependency Injection,依赖注入)是 IoC 的具体实现方式:类只「声明」自己需要什么,对象由容器塞(注入)进来。
@Service
public class OrderService {
private final PhoneNotifier notifier; // final:注入后不许换
// 构造器注入(推荐写法!):Spring 自动把容器里的 PhoneNotifier 传进来
public OrderService(PhoneNotifier notifier) {
this.notifier = notifier;
}
public void createOrder() {
notifier.send("订单已创建");
}
}
注意你没有写任何 new——OrderService 被 Spring 创建时,Spring 发现构造器需要一个 PhoneNotifier,就先去容器找(没有就先创建一个),然后传进来。整条链路全自动。
DI 一共有三种写法:
| 方式 | 写法 | 评价 |
|---|---|---|
| 构造器注入 | 构造方法参数 | ✅ 推荐:字段可 final、依赖一目了然、方便单元测试 |
| Setter 注入 | setXxx 方法 + @Autowired | 用于「可选依赖」,少见 |
| 字段注入 | @Autowired private PhoneNotifier notifier; | 写法最短但缺陷多:不能 final、藏起依赖、难测试。新项目别用 |
3.4 解耦的威力:面向接口编程
依赖注入真正的威力,要配合「接口」才能发挥:
public interface Notifier { // 1. 先定义"通知"这个能力
void send(String message);
}
@Component
public class PhoneNotifier implements Notifier { // 2. 手机短信实现
public void send(String message) { /* 调短信接口 */ }
}
@Service
public class OrderService {
private final Notifier notifier; // 3. 依赖的是接口,不是具体类!
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
}
哪天产品要求改成「邮件通知」,你只需新写一个 EmailNotifier implements Notifier 并让它成为首选 Bean(加 @Primary 注解),OrderService 一行代码都不用动——因为它依赖的是「通知」这个抽象能力,而不是某个具体实现。这就是传说中的解耦:组件像乐高积木,可插拔。
3.5 再进阶一步(了解即可)
- 配置类与 @Bean:第三方库的类你改不了源码、贴不了注解,可以在配置类里用方法造 Bean:
@Bean public RestTemplate restTemplate(){ return new RestTemplate(); }; - 作用域:默认所有 Bean 是单例(全容器一份,大家共用)。加
@Scope("prototype")可改成每次要新的。日常开发用默认即可; - 生命周期:Bean 从「实例化 → 注入依赖 → 初始化 → 使用 → 销毁」全程由容器照管,可用
@PostConstruct在初始化后插一段自己的逻辑(比如启动时加载缓存)。导入注意:Spring Boot 3 起这个注解属于 Jakarta EE,必须import jakarta.annotation.PostConstruct;;网上老代码里的javax.annotation.*已整体改名为jakarta.annotation.*,照抄会直接编译失败——这就是「新旧教程混用」最常见的坑。
✍️ 自测:@Service 和 @Component 有什么区别?
答:功能上没有区别(@Service 本质就是 @Component 的语义化别名),区别在「语义」:@Service 告诉团队成员「这是业务逻辑类」,让代码结构一眼可读。同理还有 @Repository(数据层)、@Controller(Web 层)。
✍️ 自测:为什么构造器注入优于字段注入?
答:三个理由:① 字段可以声明成 final,保证依赖不被篡改;② 依赖关系在构造器签名里一目了然(一眼看出这个类需要什么);③ 单元测试时可以直接 new OrderService(假Notifier),不需要启动 Spring 容器。
第 4 章Spring MVC:Web 前台
用户在浏览器敲下一个网址,请求是怎么「精准送达」你写的某段代码的?本章揭晓。
4.1 一次 HTTP 请求的完整旅程
Spring MVC 的核心是一个叫 DispatcherServlet(前端控制器)的组件——所有请求先到它这里报到,它像前台总机一样查表分发。下面的图值得反复看,它就是整个 Java Web 的骨架:
图 4-1 · 一个请求的往返全程:去时逐层下钻,回时原路逆袭
4.2 写出第一个接口
@RestController // 声明:这是接口类,返回值自动转 JSON
public class HelloController {
@GetMapping("/hello") // 处理 GET 请求,路径 /hello
public String hello() {
return "Hello, Spring MVC!";
}
}
项目运行后,浏览器访问 http://localhost:8080/hello,页面显示那句话。对比第 2 章的 Servlet 写法:十几行配置和样板代码,浓缩成了两个注解。
@RestController=@Controller(我是 Web 层)+@ResponseBody(我的返回值不是网页,直接写进响应体,转成 JSON);@GetMapping("/hello")= 把「GET 请求 + /hello 路径」绑定到下面这个方法。DispatcherServlet 就是靠这些登记信息建「路由表」的。
4.3 RESTful:一套通用的接口设计规范
企业开发普遍遵循 RESTful 风格——URL 只用名词表示资源,用 HTTP 动词表示操作。以「用户」资源为例:
| 操作 | 请求方式 | URL | 说明 |
|---|---|---|---|
| 查列表 | GET | /users | 分页参数放问号后:/users?page=1&size=10 |
| 查单个 | GET | /users/1 | 1 是路径参数 |
| 新增 | POST | /users | 数据放请求体(JSON) |
| 整体修改 | PUT | /users/1 | 数据放请求体 |
| 删除 | DELETE | /users/1 | 一般只带 id |
对应的 Controller 长这样(这是你会写一辈子的模板):
@RestController
@RequestMapping("/users") // 类级前缀:下面所有接口都以 /users 开头
public class UserController {
private final UserService userService;
public UserController(UserService userService) { // 构造器注入(第 2 章)
this.userService = userService;
}
@GetMapping // GET /users
public List<User> list() { return userService.list(); }
@GetMapping("/{id}") // GET /users/1
public User getById(@PathVariable Long id) { return userService.getById(id); }
@PostMapping // POST /users
public User create(@RequestBody User user) { return userService.create(user); }
@PutMapping("/{id}") // PUT /users/1
public User update(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
@DeleteMapping("/{id}") // DELETE /users/1
public void delete(@PathVariable Long id) { userService.delete(id); }
}
4.4 取参数三件套(重点!)
HTTP 请求里的数据藏在三个地方,各有对应的注解:
| 数据位置 | 例子 | 注解 |
|---|---|---|
| URL 路径里 | /users/1 | @PathVariable |
| 问号后查询串 | /users/search?keyword=tom&page=2 | @RequestParam |
| 请求体(JSON) | POST/PUT 的 body | @RequestBody |
| 请求头 | Authorization: xxx | @RequestHeader(登录令牌常用) |
@GetMapping("/search")
public List<User> search(
@RequestParam String keyword, // 必填
@RequestParam(defaultValue = "1") Integer page) { // 可选,给默认值
return userService.search(keyword, page);
}
@PostMapping("/register")
public Result register(@RequestBody RegisterForm form) {
// 前端发的 {"username":"tom","age":18} 自动变成 form 对象
return userService.register(form);
}
req.getParameter + 手动转类型 + 手动 new 对象;现在 Spring MVC 自动完成「类型转换 + 对象组装」。字段类型不匹配或 JSON 格式错误时,框架还会自动返回 400 响应。4.5 返回 JSON 与统一返回格式
方法返回对象时,Spring MVC 自动用 Jackson 库把它序列化成 JSON。企业项目通常再包一层「统一返回格式」,让前端永远拿到同样的结构:
public class Result<T> { // T 表示"泛型":data 里装什么类型,用的时候再定
private int code; // 200=成功,400=客户端错,500=服务器错
private String message; // 提示信息
private T data; // 真正的数据
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<T>();
r.code = 200; r.message = "success"; r.data = data;
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<T>();
r.code = code; r.message = message;
return r;
}
// getter/setter 省略(IDEA 右键 Generate 可自动生成)
}
返回给前端的 JSON 长这样:
{ "code": 200, "message": "success", "data": { "id": 1, "username": "tom" } }
4.6 全局异常处理:try-catch 只写一次
业务代码里到处 try-catch 很难看。Spring MVC 提供 @RestControllerAdvice:全项目的异常都汇到这里统一处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class) // 捕获所有运行时异常
public Result<Void> handleBiz(RuntimeException e) {
return Result.error(400, e.getMessage()); // 业务错误 → 400 + 原因
}
@ExceptionHandler(Exception.class) // 兜底:其他所有异常
public Result<Void> handleOther(Exception e) {
return Result.error(500, "服务器开小差了,请稍后再试");
}
}
之后 Service 里只管 throw new RuntimeException("用户名已存在"),接口会自动返回 {"code":400,"message":"用户名已存在"}。Controller、Service 层再也不用写 try-catch。
4.7 拦截器:请求的「安检门」(了解)
想在请求到达 Controller 之前统一做检查(比如登录校验)?用拦截器:
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
String token = req.getHeader("Authorization");
if (token == null || token.isBlank()) {
resp.setStatus(401); // 未登录
return false; // false = 拦下,请求到此为止
}
return true; // true = 放行
}
}
注册后,所有被拦截路径的请求都要先过这道安检门。学完第 9 章的 JWT 后,你会把它升级成真正的登录系统。
4.8 三层架构:项目的标准结构
图 4-2 · 三层架构:职责分离,只能自上而下逐层调用
为什么分三层?换前端不用动业务、改业务不用动数据库、每层可单独测试。第 7 章实战将严格按这个结构写。
✍️ 自测:GET /users/1 里的「1」、?page=2 里的「2」、POST 请求体里的 JSON,分别用哪个注解接收?
答:路径里的 1 → @PathVariable Long id;查询串里的 2 → @RequestParam Integer page;请求体 JSON → @RequestBody User user。
✍️ 自测:@RestController 和 @Controller 的区别?
答:@Controller 是传统写法,方法返回值通常被解析成页面路径;@RestController = @Controller + @ResponseBody,每个方法的返回值都直接序列化成 JSON 写进响应体。前后端分离项目一律用 @RestController。
第 5 章MyBatis:数据库工具
会写 SQL 就会用 MyBatis——它把 JDBC 的脏活累活全包了。
5.1 MyBatis 帮你干了什么
回顾第 2 章的 JDBC 代码:获取连接、预编译、填参数、取结果、关资源,五步里四步是模板。MyBatis 的分工是:
图 5-1 · MyBatis 执行流程:你只负责「调用方法」,其余全自动
MyBatis 被称为「半自动 ORM 框架」:SQL 由你亲手写(可控、可优化、DBA 能审),执行和「记录 ↔ 对象」的映射由它包办。相比之下 JPA/Hibernate 是「全自动」(连 SQL 都帮你生成)。国内企业 MyBatis 是绝对主流。
5.2 注解版:一个接口搞定一张表的 CRUD
@Mapper // 告诉 MyBatis:为这个接口生成实现类
public interface UserMapper {
@Select("SELECT id, username, age, email, create_time FROM users WHERE id = #{id}")
User selectById(Long id); // 结果自动装进 User 对象!
@Select("SELECT id, username, age, email, create_time FROM users ORDER BY id DESC")
List<User> selectAll();
@Insert("INSERT INTO users(username, age, email) VALUES(#{username}, #{age}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id") // 自增 id 回填到 user.id
int insert(User user); // #{username} 自动从 user 对象取同名字段
@Update("UPDATE users SET username = #{username}, age = #{age} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(Long id);
}
注意:你没有写实现类!调用 userMapper.selectById(1L) 时,MyBatis 在运行期动态生成实现:拼 SQL → 执行 → 按列名把值填进 User 对象 → 返回。条件就一个:实体类字段名和数据库列名能对上(对不上的见 5.5 节)。
5.3 #{} 与 ${}:必考的安全题
这两个占位符长得像,安全性天差地别:
图 5-2 · 同样的恶意输入,#{} 挡住攻击,${} 引狼入室
ORDER BY 列名等「不能加引号的位置」才用 ${},且必须用白名单校验(先判断输入是否在允许的列名集合里)。5.4 动态 SQL:传了就查,不传就跳过
真实需求经常是「多个筛选条件,填哪个查哪个」。这种 SQL 没法用注解优雅表达,改用 XML 写法。先在 application.yml 声明 XML 存放位置:
mybatis:
mapper-locations: classpath:mapper/*.xml # 扫描 resources/mapper 下的所有 xml
configuration:
map-underscore-to-camel-case: true # 下划线列名 ↔ 驼峰字段名自动互转
然后在 resources/mapper/UserMapper.xml 写动态 SQL(接口里放对应方法声明,方法名 = 标签 id):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper"> <!-- 绑定到哪个接口 -->
<select id="search" resultType="com.example.demo.entity.User">
SELECT id, username, age, email FROM users
<where> <!-- 智能处理开头的 AND -->
<if test="keyword != null and keyword != ''"> <!-- 传了才拼这句 -->
AND username LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</where>
ORDER BY id DESC
</select>
</mapper>
常用动态标签一览:
| 标签 | 用途 | 例子 |
|---|---|---|
<if> | 条件成立才拼接 | 上面的 keyword 判断 |
<where> | 自动补 WHERE、删掉开头多余的 AND/OR | 配合多个 if |
<set> | 智能 UPDATE:自动补 SET、去掉末尾多余逗号 | 「只更新传了的字段」 |
<foreach> | 遍历集合 | WHERE id IN (1,2,3)、批量插入 |
<choose>/<when>/<otherwise> | 多选一(相当于 switch) | 不同角色走不同查询 |
<if> 等动态逻辑的用 XML。两者可在同一个 Mapper 里混用。5.5 列名与字段名对不上怎么办
数据库习惯用下划线命名(create_time),Java 习惯驼峰(createTime)。两种解法:
- 推荐:开配置
map-underscore-to-camel-case: true(见 5.4),全局自动互转,一劳永逸; - 手动:复杂映射(如多表联查、字段名完全不同)用
<resultMap>显式声明「哪列装进哪个字段」。
<resultMap id="userMap" type="com.example.demo.entity.User">
<id property="id" column="id"/>
<result property="createTime" column="create_time"/>
<result property="username" column="username"/>
</resultMap>
<select id="selectById" resultMap="userMap">
SELECT * FROM users WHERE id = #{id}
</select>
5.6 分页:PageHelper 两行搞定
列表页都要分页。引入 PageHelper 插件后,任何查询前加一行代码即自动分页:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version> <!-- 2.x 适配 Spring Boot 3 -->
</dependency>
PageHelper.startPage(pageNum, pageSize); // 紧跟的第一条查询自动被分页
List<User> users = userMapper.selectAll();
PageInfo<User> info = new PageInfo<>(users); // 拿到总数、总页数、当前页…
✍️ 自测:下面哪条有 SQL 注入风险?
A. WHERE name = #{name} B. ORDER BY ${column}(column 来自用户输入且未校验)
答:B。${} 是纯字符串拼接,用户传入恶意内容会直接改变 SQL 语义。A 是预编译参数,安全。若必须动态排序列,先校验 column 是否在白名单(如 ["id","age","create_time"])内。
第 6 章Spring Boot:总装车间
前三章的三大件各自好用,但拼装起来曾让无数人掉光头发。Spring Boot 一句「我全包了」。
6.1 没有 Boot 的年代:配置地狱
把 Spring + Spring MVC + MyBatis 整合到一起,老一代程序员要手写几十个 XML,感受一下当年的一部分:
<!-- 这只是当年配置的冰山一角(认识即可,不用学) -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.demo.mapper"/>
</bean>
<!-- 还有 DispatcherServlet 注册、组件扫描、事务管理器、视图解析器…… -->
更要命的是依赖版本:Spring 5.2 要配 MyBatis 3.5.x,log4j 又要排除冲突……新手一半时间耗在「让项目能启动」上。
6.2 Boot 三板斧
① Starter 起步依赖:一个坐标拉一串
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <!-- 这一行 = -->
</dependency>
<!-- spring-mvc + jackson(JSON) + 内嵌tomcat + 日志 + ...,版本全部兼容 -->
官方把「做某类事需要的依赖组合」打包成 starter:做 Web 用 starter-web,连数据库用 starter-jdbc,测试用 starter-test。从此告别版本冲突地狱。
② 自动配置:看到什么就配什么
图 6-1 · 自动配置:Boot 侦察你的项目,按需把 Bean 全部配好——这就是「约定大于配置」
③ 内嵌服务器:main 方法一跑,网站就通了
以前要单独装 Tomcat、把项目打成 war 扔进去。Boot 把 Tomcat 做成项目的一个依赖,启动类运行即内嵌启动。这也是「为什么你能用 main 方法启动一个网站」的答案。
6.3 启动类:整个项目只有 7 行
@SpringBootApplication // 三合一:自动配置 + 组件扫描 + Bean 注册
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication 只扫描启动类所在包及子包。启动类必须放在最上层,例如:6.4 application.yml:一个文件管所有配置
YAML 用「缩进」表达层级(必须空格,禁止 Tab),比 XML 清爽十倍:
server:
port: 8080 # 冒号后必须有一个空格!
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: "123456" # 密码含特殊字符时加引号
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
常用配置速查:
| 想干什么 | 配置项 |
|---|---|
| 改端口 | server.port: 9090 |
| 连数据库 | spring.datasource.url / username / password |
| MyBatis XML 位置 | mybatis.mapper-locations |
| 驼峰映射 | mybatis.configuration.map-underscore-to-camel-case |
| 打印 SQL 日志(调试神器) | logging.level.com.example.demo.mapper: debug |
| JSON 日期格式 | spring.jackson.date-format: yyyy-MM-dd HH:mm:ss |
6.5 多环境配置(开发/生产一套代码)
开发用本地库、上线用服务器库?拆成多个文件,激活哪份用哪份:
# application.yml(公共)
spring:
profiles:
active: dev # 当前激活 dev 环境;上线时改成 prod
# application-dev.yml(开发环境)
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai
username: root
password: "123456"
# application-prod.yml(生产环境)
spring:
datasource:
url: jdbc:mysql://prod-server:3306/demo?serverTimezone=Asia/Shanghai
username: app
password: "生产环境的复杂密码"
6.6 打包部署:一行命令变出一个可执行 jar
mvn package -DskipTests # 打包(跳过测试)
java -jar target/demo-0.0.1-SNAPSHOT.jar # 服务器上有 JDK17 即可运行
把这一个 jar 传到云服务器,java -jar 一跑,网站就上线了——这就是 Spring Boot 项目「一个 jar 走天下」的部署方式。还可以加 spring-boot-starter-actuator 依赖获得健康检查等运维接口(/actuator/health)。
✍️ 自测:为什么 Spring Boot 项目没有 web.xml、没有 Tomcat 安装目录也能跑?
答:① starter-web 已把 tomcat 作为依赖打进项目(内嵌容器);② DispatcherServlet 等组件由自动配置机制按约定注册。所以 main 方法一跑,内嵌 Tomcat 随之启动,无需任何外部服务器与 XML。
第 7 章实战:用户管理系统(从零到跑通)
光学不练假把式。本章把前 5 章全部串起来,20 分钟跑通一个完整可用的后端项目。
7.1 需求与接口设计
做一个「用户管理」模块:对 users 表的增删改查,统一返回格式,统一异常处理。接口清单:
| 功能 | 请求 | 路径 | 成功返回 |
|---|---|---|---|
| 用户列表 | GET | /users | Result<List<User>> |
| 查单个用户 | GET | /users/{id} | Result<User> |
| 新增用户 | POST | /users | Result<User>(含回填的自增 id) |
| 修改用户 | PUT | /users/{id} | Result<User> |
| 删除用户 | DELETE | /users/{id} | Result<Void> |
7.2 第一步:建库建表
CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4;
USE demo;
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
age INT NULL,
email VARCHAR(100) NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users(username, age, email) VALUES
('tom', 18, 'tom@example.com'),
('jerry',22, 'jerry@example.com');
7.3 第二步:创建项目
IDEA → New Project → 选 Spring Initializr(国内慢就用 https://start.aliyun.com):
- Language: Java · Type: Maven · JDK: 17 · Group:
com.example· Artifact:demo - Dependencies 勾选:Spring Web、MySQL Driver、MyBatis Framework(都在列表里,MyBatis 在 SQL 分类下)
生成的 pom.xml 关键部分(确认这几段在):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
7.4 第三步:项目结构(最终目标)
下面按 ①→⑤ 顺序,把 7 个文件全部写完。
7.5 第四步:写代码(7 个文件)
① entity/User.java —— 实体类,和表一一对应
package com.example.demo.entity;
import java.time.LocalDateTime;
public class User {
private Long id;
private String username;
private Integer age;
private String email;
private LocalDateTime createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
@Data 注解,编译期自动生成全部 getter/setter/toString。新手建议先手写一两次理解它是什么,再引入 Lombok。② mapper/UserMapper.java —— 持久层(第 5 章知识)
package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("SELECT id, username, age, email, create_time FROM users WHERE id = #{id}")
User selectById(Long id);
@Select("SELECT id, username, age, email, create_time FROM users ORDER BY id DESC")
List<User> selectAll();
@Insert("INSERT INTO users(username, age, email) VALUES(#{username}, #{age}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE users SET username = #{username}, age = #{age}, email = #{email} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(Long id);
}
③ service/UserService.java —— 业务层(第 2、3 章知识)
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) { // 构造器注入
this.userMapper = userMapper;
}
public List<User> list() {
return userMapper.selectAll();
}
public User getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new RuntimeException("用户不存在: id=" + id); // 交给全局异常处理
}
return user;
}
public User create(User user) {
if (user.getUsername() == null || user.getUsername().isBlank()) {
throw new RuntimeException("用户名不能为空");
}
userMapper.insert(user); // 自增 id 已回填到 user.id
return user;
}
public User update(Long id, User user) {
getById(id); // 先确认存在,不存在会抛异常
user.setId(id);
userMapper.update(user);
return userMapper.selectById(id);
}
public void delete(Long id) {
getById(id);
userMapper.deleteById(id);
}
}
④ controller/UserController.java —— 接口层(第 4 章知识)
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public Result<List<User>> list() {
return Result.ok(userService.list());
}
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.ok(userService.getById(id));
}
@PostMapping
public Result<User> create(@RequestBody User user) {
return Result.ok(userService.create(user));
}
@PutMapping("/{id}")
public Result<User> update(@PathVariable Long id, @RequestBody User user) {
return Result.ok(userService.update(id, user));
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.ok(null);
}
}
⑤⑥ common/Result.java 与 GlobalExceptionHandler.java(第 4 章知识)
package com.example.demo.common;
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<T>();
r.code = 200; r.message = "success"; r.data = data;
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<T>();
r.code = code; r.message = message;
return r;
}
public int getCode() { return code; }
public String getMessage() { return message; }
public T getData() { return data; }
public void setCode(int code) { this.code = code; }
public void setMessage(String message) { this.message = message; }
public void setData(T data) { this.data = data; }
}
package com.example.demo.common;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public Result<Void> handleBiz(RuntimeException e) {
return Result.error(400, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleOther(Exception e) {
return Result.error(500, "服务器开小差了,请稍后再试");
}
}
⑦ application.yml —— 配置文件(第 6 章知识)
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: "改成你的密码"
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.example.demo.mapper: debug # 控制台打印 SQL,调试神器
7.6 第五步:运行与测试
运行 DemoApplication,控制台出现这两行即成功:
Tomcat started on port 8080 (http) with context path ''
Started DemoApplication in 2.3 seconds (process running for 2.9)
用 curl(或 Postman)逐个测试:
# 1. 查列表
curl http://localhost:8080/users
# 2. 查单个
curl http://localhost:8080/users/1
# 3. 新增(注意 Content-Type 请求头!)
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d "{\"username\":\"spike\",\"age\":30,\"email\":\"spike@example.com\"}"
# 4. 修改
curl -X PUT http://localhost:8080/users/1 -H "Content-Type: application/json" -d "{\"username\":\"tomcat\",\"age\":19,\"email\":\"tom@example.com\"}"
# 5. 删除
curl -X DELETE http://localhost:8080/users/3
# 6. 查不存在的 id —— 体验全局异常处理
curl http://localhost:8080/users/999
期望响应(第 1、6 个):
// GET /users
{ "code": 200, "message": "success", "data": [
{ "id": 2, "username": "jerry", "age": 22, "email": "jerry@example.com", "createTime": "2026-09-01T20:15:30" },
{ "id": 1, "username": "tomcat", "age": 19, "email": "tom@example.com", "createTime": "2026-09-01T20:15:30" }
] }
// GET /users/999 → 全局异常处理接管
{ "code": 400, "message": "用户不存在: id=999", "data": null }
7.7 课后扩展任务(检验学会没有)
- 给 users 表加一个
nickname字段,同步修改实体类和所有 SQL(练:全链路加字段); - 加一个接口:按用户名模糊搜索(练:@RequestParam + LIKE);
- 用 PageHelper 给列表加分页参数(练:5.6 节);
- 把「查列表」改成 XML 动态 SQL:关键词、最小年龄都可传可不传(练:5.4 节);
- 新增用户时校验 age 必须在 0~150 之间,不满足抛异常(练:业务校验 + 全局异常)。
第 8 章新手常见坑速查表
遇到报错先来这张表对号入座,能省下大把排错时间。都是真实高频问题。
| 现象 / 报错关键词 | 原因 | 解决 |
|---|---|---|
Field userMapper ... required a bean | Mapper 接口没被扫描注册 | 接口加 @Mapper;或启动类加 @MapperScan("com.example.demo.mapper");检查启动类是否在最上层包 |
Port 8080 was already in use | 端口被占(上次进程没关) | 改 server.port;或杀掉旧进程(任务管理器找 java.exe) |
Access denied for user 'root'@'localhost' | 数据库账号或密码错误 | 核对 yml 里的 username/password;密码含特殊字符要用引号包起来 |
Unknown database 'demo' | 数据库没建 | 先执行第 7.2 节的建库 SQL |
连接报 serverTimezone / 时区错误 | MySQL 8 需要显式时区 | URL 追加 ?serverTimezone=Asia/Shanghai |
| 接口访问 404 | 路径写错 / Controller 没被扫描 | 核对 URL 与注解路径(含类级前缀);确认类上有 @RestController 且位于扫描范围内 |
| POST 返回 415 Unsupported Media Type | 没声明请求体格式 | 请求加请求头 Content-Type: application/json |
| POST 返回 400,JSON 没进对象 | JSON 字段名与实体类对不上 | 核对字段名大小写;检查是否忘了 @RequestBody |
| 查询结果某字段是 null | 列名下划线 vs 属性驼峰没对上 | 开 map-underscore-to-camel-case: true(见 5.5) |
There is no getter for property ... | #{xxx} 里写了对象没有的属性 | 核对占位符名称与实体类字段名 |
| XML 动态 SQL 不生效 | yml 没配 mapper-locations 或 id 与方法名不一致 | 确认扫描路径正确;<select id=""> 必须等于接口方法名 |
| 改了代码不生效 | IDEA 没重新编译 / 老进程还在跑 | 停掉旧进程再运行;必要时 Build → Rebuild Project |
| 中文乱码 | 连接或表编码不是 utf8mb4 | URL 加 characterEncoding=utf8;建表用 utf8mb4 |
| Maven 依赖下载极慢/失败 | 默认镜像在国外 | 按下方阿里云镜像配置改 settings.xml |
阿里云 Maven 镜像(国内必备):打开 ~/.m2/settings.xml(没有就新建),加入下面这段;老教程里的 maven.aliyun.com/nexus/content/groups/public 地址已失效,别再抄:
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>*</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
第 9 章进阶路线:接下来学什么
你已经掌握了最核心的主干。下面是真实企业开发者的成长路径。
图 9-1 · 三阶段成长路线
9.1 按顺序补齐的技能清单
- 参数校验:
@Validated+@NotNull/@Min/@Size,替代手写 if 校验,代码更专业; - 事务:
@Transactional——理解「下单扣库存」为什么必须在一个事务里(要么全成功,要么全回滚); - 登录认证:JWT + 拦截器,实现注册/登录/令牌校验,理解每个网站为什么要登录;
- Redis 缓存:把热点数据放缓存,理解「为什么 12306 首页快」;
- MyBatis-Plus:MyBatis 的增强版(国内很流行),单表 CRUD 连 SQL 都不用写,先懂 MyBatis 再用它事半功倍;
- 前端联调:学一点 Vue 或 React,用你写的接口做一个真实页面;
- 部署上线:云服务器 +
java -jar+ Nginx 反向代理,把项目发布到公网。
9.2 官方资料(遇到问题的第一手答案)
| 资源 | 地址 |
|---|---|
| Spring 官方文档 | https://spring.io/projects/spring-boot |
| MyBatis 中文文档 | https://mybatis.org/mybatis-3/zh/index.html |
| MyBatis-Spring-Boot | https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/ |
| 项目脚手架 | https://start.spring.io(国内:https://start.aliyun.com) |
附录速查卡合集
A. 注解速查(按层分类)
| 层 | 注解 | 作用 |
|---|---|---|
| Spring | @Component / @Service / @Repository | 把类登记为 Bean |
@Autowired / 构造器注入 | 注入依赖 | |
@Bean / @Configuration | 配置类中手工造 Bean | |
@Primary / @Qualifier | 多个同类 Bean 时指定用哪个 | |
| Spring MVC | @RestController | 接口类,返回值转 JSON |
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping | 路径映射 | |
@PathVariable / @RequestParam / @RequestBody | 取路径/查询/请求体参数 | |
@RestControllerAdvice + @ExceptionHandler | 全局异常处理 | |
HandlerInterceptor | 拦截器(登录校验等) | |
| MyBatis | @Mapper / @MapperScan | 声明 Mapper 接口 / 批量扫描 |
@Select / @Insert / @Update / @Delete | 注解版 SQL | |
<if> <where> <set> <foreach> | XML 动态 SQL 标签 | |
| Boot | @SpringBootApplication | 启动类三合一注解 |
@Transactional | 事务(配合 starter-data 或 spring-tx) | |
@Value / @ConfigurationProperties | 读取 yml 配置 |
B. HTTP 状态码速查
| 状态码 | 含义 | 常见触发场景 |
|---|---|---|
| 200 | 成功 | 一切正常 |
| 400 | 客户端参数错误 | JSON 格式错、校验失败、业务异常(本教程统一用它) |
| 401 | 未认证 | 没登录 / token 失效 |
| 403 | 无权限 | 登录了但权限不够 |
| 404 | 资源不存在 | URL 写错 / 方法没有对应映射 |
| 405 | 方法不允许 | 用 GET 去访问了 POST 接口 |
| 415 | 不支持的媒体类型 | POST 忘了 Content-Type: application/json |
| 500 | 服务器内部错误 | 代码抛了未处理异常 / 数据库挂了 |
C. 本教程知识主线(30 秒复盘)
Spring → IoC 容器管对象;@Service 登记 Bean;构造器注入拿 Bean;接口解耦
Spring MVC → @RestController 接请求;@XxxMapping 定路径;
@PathVariable / @RequestParam / @RequestBody 取参数;返回值自动转 JSON
MyBatis → @Mapper 接口即数据层;#{} 防注入;动态 SQL 用 XML 标签;驼峰映射要开启
Spring Boot → starter 拉依赖;自动配置免 XML;内嵌 Tomcat;yml 统一配置
实战套路 → 建表 → 实体类 → Mapper → Service → Controller → yml → 运行测试
→ 建表 → 实体 → 持久层 → 业务层 → 接口层 → 配置 → 测试
📖 Spring 全家桶零基础图文教程 · 完
有报错不可怕——它只是系统在用另一种方式教你。祝学习顺利!🚀