Spring 框架快速入门教程
Spring 框架快速入门教程 —— 从零基础到 Spring Boot 的必经之路
学习目标:从 Java 基础语法,到理解 Spring 到底解决什么问题,掌握 IoC、DI、AOP 三大核心,能独立写出基于 Spring 的中小项目,为学习 Spring Boot 铺平道路 学习方式:本人想录制一个最新版的课程结合ai开发一个功能,尝试写一下教程,本文档主要用kimi生成,后面改改,格式,
大致学习以下几点
-
第一章:Spring 是什么?先建立大局观
-
第二章:没有 Spring 之前,Java 开发有多痛苦?
-
第三章:核心思想 IoC(控制反转)—— 把"创建对象"的权力交出去
-
第四章:依赖注入 DI —— 彻底搞懂"把类当参数传"和 Spring 的区别
-
第五章:环境搭建与第一个 Spring 程序
-
第六章:XML 配置方式详解(理解原理必学)
-
第七章:注解方式(现代开发的主流)
-
第八章:Bean 的作用域与生命周期
-
第九章:AOP 面向切面编程(大白话版,一次讲透)
-
第十章:Spring 操作数据库(JdbcTemplate)
-
第十一章:声明式事务 —— AOP 最经典的应用
-
第十二章:Spring MVC 简介(通往 Spring Boot 的桥梁)
-
第十三章:常见面试题与学习路线
第一章:Spring 是什么?先建立大局观,先明白这是什么,能干什么?
1.1 一句话定义
Spring 是一个"对象管家"框架:它帮你创建对象、管理对象之间的关系、给对象统一附加通用功能,让你专心写业务代码。
官方一点的说法:Spring 是一个轻量级的 Java 企业级开发框架,核心是 IoC 容器(管理对象)和 AOP(面向切面编程),并在此之上提供了数据访问、事务管理、Web MVC 等一系列能力。
1.2 一个生活类比
想象你开了一家餐馆:
-
没有 Spring:你既是老板,又要自己去买菜(new 对象)、自己配菜(组装依赖)、自己洗碗(处理日志、事务)。菜炒得再好,人也累死了。
-
有了 Spring:你雇了一个大管家(IoC 容器)。你只需在菜单(配置文件/注解)上写明"我要做什么菜",买菜、配菜、洗碗全由管家安排。你只管炒菜(写业务逻辑)。
1.3 Spring 家族全景图
plain
Spring Framework(本教程的主角,一切的地基)
├── Spring Core(IoC 容器、DI) ← 核心中的核心
├── Spring AOP(面向切面) ← 核心之二
├── Spring JDBC / 事务管理
├── Spring MVC(Web 开发)
└── ...
Spring Boot(在 Spring 之上,自动配置、快速启动) ← 你下一步要学的
Spring Cloud(在 Spring Boot 之上,微服务全家桶) ← 以后的事
重要认知:Spring Boot 不是替代 Spring,而是"配置自动化的 Spring"。 你在 Spring Boot 里写的每一个@Autowired、@Service、@Transactional,全部来自本教程要讲的内容。地基不牢,学 Boot 时只会背注解,不懂原理,一报错就懵。
第二章:没有 Spring 之前,Java 开发有多痛苦?
学框架最怕"不知道为什么学"。我们先写一段不用 Spring 的代码,看看痛点在哪。
2.1 场景:一个用户注册功能
按经典三层结构:Controller → Service → Dao
java
// ========== 数据访问层:负责操作数据库 ==========
public class UserDao {
public void save(String username) {
System.out.println("把用户 " + username + " 存入数据库");
}
}
// ========== 业务层:负责业务逻辑 ==========
public class UserService {
// 痛点 1:自己 new 对象,Service 和 UserDao 死死绑在一起
private UserDao userDao = new UserDao();
public void register(String username) {
System.out.println("开启事务..."); // 痛点 2:事务代码和业务代码混着写
userDao.save(username);
System.out.println("提交事务...");
}
}
// ========== 控制层 ==========
public class UserController {
private UserService userService = new UserService(); // 又 new 一个
public void handleRequest() {
userService.register("张三");
}
}
2.2 这段代码的四大痛点
痛点一:对象满天飞,关系自己管
每个类都自己
new 出它需要的对象。项目一大,几百个类互相 new,谁依赖谁全靠人脑记。痛点二:改一处,动全身
假设项目升级,要改用
UserDaoMysql(原来的 UserDao 是假实现):java
// 原来的写法
private UserDao userDao = new UserDao();
// 现在必须改成
private UserDaoMysql userDao = new UserDaoMysql();
如果 50 个类都
new UserDao(),就要改 50 个文件!这就是"高耦合"——代码和具体实现焊死了。痛点三:通用代码到处复制
日志、事务、权限检查这些代码,每个方法都要写一遍(上面 register 里的"开启事务")。100 个业务方法就复制 100 遍,改的时候改 100 处。
痛点四:没法测试
Service 里写死了
new UserDao(),单元测试时想换成假的 Dao(不连真数据库)都换不了。2.3 Spring 怎么解决?
表格
| 痛点 | Spring 的解决方案 |
|---|---|
| 对象自己 new,关系乱 | IoC 容器统一创建和管理对象 |
| 代码焊死,换实现要改源码 | 依赖注入(DI):面向接口编程,实现类由配置决定 |
| 日志/事务代码到处复制 | AOP:通用功能写一次,自动织入所有需要的地方 |
| 没法测试 | 依赖从外部注入,测试时注入假实现即可 |
接下来三章,我们逐个击破。
第三章:核心思想 IoC(控制反转)—— 把"创建对象"的权力交出去
3.1 什么是 IoC?
IoC(Inversion of Control,控制反转):对象不再由自己创建,而是交给 Spring 容器创建和管理。
"反转"反的是什么?反的是创建对象的控制权:
plain
传统方式: 我(程序员写的类)→ 主动去 new → 对象 【我正控】
IoC 方式: Spring 容器 → 创建好对象 → 我伸手要 【反过来了】
被 Spring 容器管理的对象,有个专门的名字,叫 Bean。你可以把 Spring 容器想象成一个"对象仓库 + 管家":
-
你告诉管家:仓库里要备货哪些对象(通过配置或注解声明)
-
管家在程序启动时就把这些对象 new 好,放在仓库里
-
你需要时,不用 new,直接找管家要
3.2 伪代码理解 IoC 容器内部干了什么
plain
Spring 容器启动时(中文伪代码):
读取配置(XML 或注解扫描)
发现 需要管理 UserDao
发现 需要管理 UserService,且它依赖 UserDao
遍历所有需要管理的类:
1. 通过反射 调用无参构造方法,创建对象 // 相当于 new UserDao()
2. 把对象放进一个大 Map 里:
容器Map.put("userDao", userDao对象)
容器Map.put("userService", userService对象)
3. 检查每个对象有没有声明依赖别的对象
发现 userService 里有个字段需要 userDao
通过反射把 userDao对象 塞进去 // 相当于 userService.setUserDao(userDao)
程序运行时:
你调用 容器.getBean("userService")
容器从 Map 里拿出来给你 —— 拿来就能用,依赖已经装好了
看明白了吗?IoC 容器本质上就是一个 大 Map + 反射。 它替你做完了所有new和"组装"的工作。
3.3 为什么这样就解决了痛点?
回忆痛点二:要把
UserDao 换成 UserDaoMysql。用了 IoC 之后:
-
对象不是你 new 的,是容器 new 的
-
你只需把配置里的"备货物种"从 UserDao 改成 UserDaoMysql
-
50 个使用它的类,一行代码都不用改
控制权交出去,换来的是"换实现不改代码"。
第四章:依赖注入 DI —— 彻底搞懂"把类当参数传"和 Spring 的区别
很多同学(包括工作几年的人)都会问:"依赖注入不就是把一个类作为参数传给另一个类吗?以前的 Java 也能做到啊,有啥了不起?" 这一章专门把这个疑问彻底讲透。
4.1 DI 是什么
DI(Dependency Injection,依赖注入):一个对象需要的其他对象(依赖),不由自己创建,而是由容器"注射"进来。
DI 是 IoC 思想的具体实现手段。两者关系:IoC 是目标(控制权反转),DI 是手段(怎么实现反转)。
4.2 三种注入方式
java
public class UserService {
private UserDao userDao; // 注意:不 new 了!只是声明"我需要它"
// 方式一:构造方法注入(官方最推荐)
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 方式二:setter 方法注入
// public void setUserDao(UserDao userDao) {
// this.userDao = userDao;
// }
// 方式三:字段注入(注解直接标在字段上,第七章讲)
}
4.3 灵魂拷问:这和"手动把类当参数传"有什么区别?
你说得对,单看一次传参,确实没区别:
java
// 不用 Spring,我也能手动注入
UserDao dao = new UserDao();
UserService service = new UserService(dao); // 手动构造注入,没毛病
区别不在"一次传参",而在 规模 和 时机:
区别一:手动传参,链条长了会崩溃
真实项目的依赖是一棵树,不是一层:
plain
Controller 依赖 ServiceA
ServiceA 依赖 ServiceB 和 ServiceC
ServiceB 依赖 Dao1 和 Dao2
ServiceC 依赖 Dao2 和 邮件组件
Dao1 依赖 数据源DataSource
...
手动组装的代码长这样:
java
DataSource ds = new DataSource("jdbc:mysql://...", "root", "123456");
Dao1 dao1 = new Dao1(ds);
Dao2 dao2 = new Dao2(ds);
MailComponent mail = new MailComponent("smtp.qq.com", 465);
ServiceB serviceB = new ServiceB(dao1, dao2);
ServiceC serviceC = new ServiceC(dao2, mail);
ServiceA serviceA = new ServiceA(serviceB, serviceC);
Controller controller = new Controller(serviceA);
这还只是 8 个对象。真实项目几百个 Bean,你要写几百行组装代码,而且顺序不能错(必须先 new 出 DataSource 才能 new Dao1)。这些"组装代码"本身没有业务价值,纯体力活。
用 Spring:你只声明每个类"我需要什么",组装顺序、创建时机,容器自动算。
区别二:手动传参,改实现依然要改代码
手动方式里,
new UserDao() 这行代码还是写在你手里的某个地方。要换成 UserDaoMysql,你得找到那段组装代码去改。Spring 里,"用哪个实现"写在配置里(XML 的一行,或注解的一次选择),改配置不改 Java 源码。
区别三:容器顺手送你一堆增值服务
Spring 创建的 Bean,不只是个裸对象。容器可以顺手给它加上:
-
单例管理(全系统只有一个实例,省内存)
-
生命周期回调(创建后、销毁前自动执行你指定的方法)
-
AOP 代理(自动加事务、加日志,第九章讲)
-
循环依赖检测、懒加载……
手动
new 出来的对象,这些全都没有。4.4 一句话总结
手动传参 = 你自己当管家,8 个对象还行,800 个对象累死你; Spring DI = 雇个专业管家,你只写"需求清单",组装、换货、增值服务全包。
第五章:环境搭建与第一个 Spring 程序
5.1 上机需要什么环境
表格
| 项目 | 要求 | 说明 |
|---|---|---|
| JDK | 8 或以上(推荐 8/11/17) | Spring 5.x 支持 JDK 8+;Spring 6 要求 JDK 17+ |
| 构建工具 | Maven | 自动下载 Spring 的 jar 包 |
| IDE | IDEA(推荐)或 Eclipse / VS Code | 本教程截图以 IDEA 为准 |
| 网络 | 能访问 Maven 中央仓库 | 国内建议配阿里云镜像,下载快 |
本教程代码基于 Spring Framework 5.3.x + JDK 8,这是目前企业存量项目中最常见的组合,学懂了换 6 只是升级版本号的事。
5.2 创建 Maven 项目并引入 Spring
在 IDEA 中:File → New → Project → Maven → 填 GroupId(如
com.example)、ArtifactId(如 spring-demo)→ 完成。打开
pom.xml,加入依赖:xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一字符编码与 JDK 版本,避免中文乱码和编译警告 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring.version>5.3.30</spring.version>
</properties>
<dependencies>
<!--
Spring 核心容器:IoC 的本体
引入这一个,Maven 会自动连带下载:
spring-core(核心工具)、spring-beans(Bean管理)、
spring-context(容器)、spring-expression(SpEL表达式)
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
写完后点 IDEA 右侧 Maven 面板的刷新按钮,Maven 会自动下载 jar 包。
5.3 第一个程序:让容器给你一个对象
第 1 步:写一个普通类
java
package com.example;
// 一个普普通通的类,不需要继承任何 Spring 的类,这就是"轻量级"
public class HelloService {
public void sayHello() {
System.out.println("你好,Spring!这是我从容器里拿到的第一个对象。");
}
}
第 2 步:写配置文件,告诉容器"帮我管理这个类"
在
src/main/resources 目录下新建 applicationContext.xml:xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- beans 是根标签,xmlns 是命名空间声明(固定写法,照抄即可) -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
向容器注册一个 Bean:
id = 给这个对象起的名字,相当于仓库里的货号
class = 这个对象是什么类(全类名)
容器启动时就会执行:new HelloService(),并以 "helloService" 为名存起来
-->
<bean id="helloService" class="com.example.HelloService"/>
</beans>
第 3 步:启动容器,拿对象
java
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// 1. 启动 Spring 容器,读取类路径下的配置文件
// 这一刻,配置文件里声明的所有 Bean 都被创建好了
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
// 2. 从容器里拿对象:按"货号"取货
// 注意:这里完全没有 new!对象是容器早就造好的
HelloService service = (HelloService) context.getBean("helloService");
// 3. 正常使用
service.sayHello();
}
}
运行结果:
plain
你好,Spring!这是我从容器里拿到的第一个对象。
5.4 回顾:这个小程序里发生了什么
plain
main 方法启动
│
▼
new ClassPathXmlApplicationContext("applicationContext.xml")
│ ① 容器读取 XML
│ ② 发现 <bean id="helloService" class="com.example.HelloService"/>
│ ③ 反射执行 new HelloService()
│ ④ 存入容器内部的大 Map:{"helloService" → 对象}
▼
context.getBean("helloService")
│ ⑤ 从 Map 里查出对象,返回给你
▼
service.sayHello() ← 正常使用,全程零 new
恭喜,你已经跨过了 Spring 的大门。后面所有内容,都只是在这个模型上"加功能"。
第六章:XML 配置方式详解(理解原理必学)
虽然现在主流是注解,但 XML 方式能让你把注入过程看得一清二楚,是理解原理的最佳路径。注解只是把这些配置"搬家"到了类上面。
6.1 准备接口和实现类
java
package com.example.dao;
// 面向接口编程:Service 只认识接口,不认识具体实现
public interface UserDao {
void save(String username);
}
java
package com.example.dao.impl;
import com.example.dao.UserDao;
// MySQL 版本的实现
public class UserDaoMysql implements UserDao {
@Override
public void save(String username) {
System.out.println("[MySQL] 保存用户:" + username);
}
}
java
package com.example.dao.impl;
import com.example.dao.UserDao;
// Oracle 版本的实现(用来演示"换实现不改 Java 代码")
public class UserDaoOracle implements UserDao {
@Override
public void save(String username) {
System.out.println("[Oracle] 保存用户:" + username);
}
}
6.2 setter 注入(最常用、最直观)这儿核心的方式是这个方法set的方方法必须是严格命 名,要不然会报错,粗看到和构造的方式都 几乎一样。区别后面在总结
java
package com.example.service;
import com.example.dao.UserDao;
public class UserService {
// 声明依赖的是【接口】,不是具体实现 —— 这是解耦的关键
private UserDao userDao;
// Spring 会通过这个 setter 把实现类"注射"进来
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void register(String username) {
userDao.save(username); // 直接用,它已经是装好的
}
}
配置文件:
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 注册 Dao 实现类:想换数据库?改这一行的 class 就行 -->
<bean id="userDao" class="com.example.dao.impl.UserDaoMysql"/>
<!--
注册 Service,并通过 <property> 完成 setter 注入:
name = UserService 里的属性名(对应 setUserDao,去掉 set 首字母小写)
ref = 引用容器里哪个 Bean(填对方 id)—— 对象类型用 ref
-->
<bean id="userService" class="com.example.service.UserService">
<property name="userDao" ref="userDao"/>
</bean>
</beans>
测试:
java
public class Main {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
// 拿 Bean 时可以顺便指定类型,免去强转
UserService service = context.getBean("userService", UserService.class);
service.register("张三");
}
}
输出:
[MySQL] 保存用户:张三现在演示解耦的威力:把 XML 里 UserDaoMysql 改成 UserDaoOracle,重新运行,输出立刻变成
[Oracle] 保存用户:张三——Java 代码一行没动。这就是 IoC + DI 的价值。6.3 构造方法注入
java
public class UserService {
private final UserDao userDao;
// 通过构造方法接收依赖
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void register(String username) {
userDao.save(username);
}
}
XML 对应写法:
xml
<bean id="userService" class="com.example.service.UserService">
<!--
constructor-arg = 给构造方法传参
ref 指向容器里的 userDao
有多个参数就写多个 constructor-arg,顺序与构造方法一致
-->
<constructor-arg ref="userDao"/>
</bean>
setter 注入 vs 构造注入怎么选?
表格
| 维度 | 构造注入 | setter 注入 |
|---|---|---|
| 依赖缺失时 | 对象根本造不出来,启动就报错(好事) | 可能造出"缺胳膊"的对象,运行时才炸 |
| 强依赖(必须有的) | 推荐用构造注入 | 不推荐 |
| 可选依赖 | 不适合 | 适合 |
| Spring 官方态度 | 推荐 | 兼容历史项目 |
6.4 注入普通值(字符串、数字)
Bean 里不只有对象依赖,还有配置值:
java
public class MailSender {
private String host; // 邮件服务器地址
private int port; // 端口
public void setHost(String host) { this.host = host; }
public void setPort(int port) { this.port = port; }
public void send() {
System.out.println("通过 " + host + ":" + port + " 发送邮件");
}
}
xml
<bean id="mailSender" class="com.example.MailSender">
<!-- 普通值用 value,对象引用用 ref —— 别记混 -->
<property name="host" value="smtp.qq.com"/>
<property name="port" value="465"/>
</bean>
这就是"配置外置":服务器地址变了改 XML 就行,不用改代码重新编译。
6.5 注入集合
xml
<bean id="team" class="com.example.Team">
<!-- List 类型 -->
<property name="members">
<list>
<value>张三</value>
<value>李四</value>
</list>
</property>
<!-- Map 类型 -->
<property name="config">
<map>
<entry key="env" value="dev"/>
<entry key="timeout" value="3000"/>
</map>
</property>
</bean>
构造注入是 Spring 调用带参数的构造方法,在对象 new 出来的瞬间就把依赖塞进去,还能靠 final 关键字防止后续被篡改,缺依赖直接抛异常终止容器启动;setter 注入是先调用无参构造把对象创建好,
再反射调用 setUserDao 方法传依赖,允许先拿到半成品对象,后续再补全依赖,更适合处理可选参数或者循环注入的场景。Spring 的 XML setter 注入严格遵循 JavaBean 规范,方法名必须以 set 开头、
后跟首字母大写的属性名,你把 set 去掉写成 userDao,Spring 反射时找不到匹配的 setter 方法,直接会报无法找到对应写入方法的异常。
第七章:注解方式(现代开发的主流)
XML 配 200 个 Bean 就是 200 行
<bean>,太啰嗦。于是 Spring 2.5 之后主推注解:把配置信息直接写在类上,容器启动时自动扫描并注册。7.1 核心注解一览
表格
| 注解 | 写在哪 | 作用 |
|---|---|---|
@Component |
类上 | "我是一个 Bean,请容器管理我"(通用) |
@Controller |
类上 | 同上,但用于 Web 控制层(语义区分) |
@Service |
类上 | 同上,用于业务层 |
@Repository |
类上 | 同上,用于数据访问层 |
@Autowired |
字段/构造方法/setter 上 | "请把依赖自动注射进来" |
@Value |
字段上 | 注入普通值 |
@Configuration |
类上 | "我是一个配置类,作用等同 XML 文件" |
@ComponentScan |
配置类上 | 指定扫描哪些包 |
四个"组件注解"(Component/Controller/Service/Repository)功能完全一样,区别只是给人看的语义:分层清晰,读代码的人一眼知道这属于哪一层。养成按层使用的习惯。
7.2 用注解重写上一章的例子
Dao 层:
java
package com.example.dao.impl;
import com.example.dao.UserDao;
import org.springframework.stereotype.Repository;
// @Repository:声明这是一个数据访问层的 Bean
// 括号里的 "userDao" 是 Bean 的名字;不写默认是类名首字母小写(userDaoMysql)
@Repository("userDao")
public class UserDaoMysql implements UserDao {
@Override
public void save(String username) {
System.out.println("[MySQL] 保存用户:" + username);
}
}
Service 层:
java
package com.example.service;
import com.example.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 声明为业务层 Bean,默认名字 userService
public class UserService {
/*
@Autowired:自动装配
容器启动时会去"仓库"里找一个 UserDao 类型的 Bean,自动塞到这个字段上
—— 相当于自动执行了 userService.setUserDao(userDao)
不需要再写 setter,也不需要 XML
*/
@Autowired
private UserDao userDao;
public void register(String username) {
userDao.save(username);
}
}
配置类(取代 XML):
java
package com.example.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/*
@Configuration:这个类等价于之前的 applicationContext.xml
@ComponentScan:告诉容器"去 com.example 包及其子包下扫描,
凡是带 @Component/@Service/@Repository/@Controller
注解的类,全部创建对象收进容器"
*/
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
启动类(注意换了一个容器实现):
java
package com.example;
import com.example.config.AppConfig;
import com.example.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// 注解方式用 AnnotationConfigApplicationContext,传入配置类
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = context.getBean(UserService.class);
service.register("李四");
}
}
输出:
[MySQL] 保存用户:李四对比一下:XML 方式要维护"配置文件 + Java 类"两份东西;注解方式信息就在类上,改类时不会忘改配置。这就是为什么现代项目(包括 Spring Boot)全面转向注解。
7.3 @Autowired 的查找规则(重要)
容器按以下顺序找"注射什么进来":
-
先按类型找:容器里有几个 UserDao 类型的 Bean?
-
只有 1 个 → 直接注入,完美。
-
有多个(比如同时存在 UserDaoMysql 和 UserDaoOracle)→ 再按字段名当 Bean 名找:
java
@Autowired
private UserDao userDaoOracle; // 字段名 = Bean 名,容器按名字精确定位
-
还搞不定,用
@Qualifier明确指定:
java
@Autowired
@Qualifier("userDaoMysql") // 明确告诉容器:我要的是叫 userDaoMysql 的那个
private UserDao userDao;
面试高频:@Autowired 默认按类型(byType)装配,类型冲突时按名称(byName)兜底。
7.4 构造注入的注解写法(推荐)
java
@Service
public class UserService {
private final UserDao userDao;
/*
构造注入:@Autowired 标在构造方法上
Spring 4.3 之后,如果类只有一个构造方法,@Autowired 可以省略
好处:字段可以声明为 final,依赖明确、不可变、方便单元测试
*/
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
7.5 @Value 注入配置值
java
@Component
public class MailSender {
@Value("smtp.qq.com") // 直接写字面值
private String host;
@Value("465")
private int port;
}
实际项目中值写在 properties 文件里:
properties
# src/main/resources/mail.properties
mail.host=smtp.qq.com
mail.port=465
java
@Configuration
@ComponentScan("com.example")
// 加载 properties 文件,让 @Value 能用 ${} 占位符取值
@PropertySource("classpath:mail.properties")
public class AppConfig {
}
java
@Value("${mail.host}") // ${} 从 properties 文件取值 —— 这就是 Spring Boot 配置文件的雏形!
private String host;
留意这个@Value("${...}"),你在 Spring Boot 里会天天和它见面。
第八章:Bean 的作用域与生命周期
8.1 作用域:容器给你几个对象?
java
// 默认情况:单例(singleton)
// 全容器只有一个实例,无论你 getBean 多少次,拿到的都是同一个对象
UserService a = context.getBean(UserService.class);
UserService b = context.getBean(UserService.class);
System.out.println(a == b); // true —— 同一个对象
用
@Scope 改变作用域:java
@Component
@Scope("prototype") // 原型模式:每次 getBean 都 new 一个新的
public class UserService { ... }
表格
| 作用域 | 含义 | 使用场景 |
|---|---|---|
singleton(默认) |
容器里唯一实例 | 绝大多数无状态组件(Service/Dao) |
prototype |
每次获取都新建 | 有状态的对象(存了用户数据的对象) |
request |
每个 HTTP 请求一个 | Web 项目 |
session |
每个会话一个 | Web 项目 |
经验法则:Service、Dao 这种"工具人"类(不存业务数据,只有方法)就用默认单例,省内存;如果对象要保存每个用户各自的数据,才考虑 prototype。
8.2 生命周期:对象从出生到销毁
plain
容器启动
│
▼
1. 实例化 反射调用构造方法(new)
│
▼
2. 依赖注入 把 @Autowired 标的地方填上
│
▼
3. 初始化回调 @PostConstruct 标注的方法被自动调用
│ (常用来加载缓存、预热连接)
▼
4. 投入使用 你的代码正常调用它
│
▼
5. 销毁回调 容器关闭时,@PreDestroy 标注的方法被调用
(常用来释放资源、关闭连接)
代码示例:
java
package com.example;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
public class CacheManager {
public CacheManager() {
System.out.println("【1. 实例化】构造方法执行,对象诞生了");
}
/*
@PostConstruct:依赖注入完成后自动执行
典型用途:启动时把字典表从数据库加载进内存
注意:此时 @Autowired 的依赖已经全部到位,构造方法里则不一定
*/
@PostConstruct
public void init() {
System.out.println("【3. 初始化】依赖已装好,开始预热缓存...");
}
/*
@PreDestroy:容器关闭前自动执行
典型用途:释放线程池、关闭文件句柄
*/
@PreDestroy
public void destroy() {
System.out.println("【5. 销毁】容器要关了,清理资源...");
}
}
java
public class Main {
public static void main(String[] args) {
// 用具体类型接收,才能调用 close()
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(CacheManager.class);
context.close(); // 关闭容器,触发销毁回调
}
}
运行输出(顺序清晰可见):
plain
【1. 实例化】构造方法执行,对象诞生了
【3. 初始化】依赖已装好,开始预热缓存...
【5. 销毁】容器要关了,清理资源...
注:JDK 9+ 移除了javax.annotation,如果报找不到@PostConstruct,在 pom.xml 补一个依赖:xml<dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency>
第九章:AOP 面向切面编程(大白话版,一次讲透)
这一章是很多初学者的噩梦,也是本教程的深水区。我们不堆术语,用"加鸡蛋"的故事讲明白。
9.1 先从一个"恐怖故事"说起
你的系统有 100 个业务方法。今天老板提了个需求:"每个方法执行前后,都给我记录日志。"
不用 AOP 的写法:
java
public void 转账() {
System.out.println("日志:转账 开始了"); // ← 加的
// ...真正的转账逻辑(可能只有 3 行)
System.out.println("日志:转账 结束了"); // ← 加的
}
public void 下单() {
System.out.println("日志:下单 开始了"); // ← 又加一遍
// ...真正的下单逻辑
System.out.println("日志:下单 结束了");
}
// ...剩下 98 个方法,重复 98 遍
三个致命问题:
-
重复:同样的日志代码复制 100 份;
-
污染:业务方法里掺着不相干的代码,读代码的人要在一堆日志里找业务;
-
改不动:老板改口说"日志前面加个时间戳",你要改 100 个地方。
9.2 AOP 的思路:把"横切"的代码抽出来,统一"织入"
观察一下:日志代码像什么?
业务代码是一条条竖线(转账、下单、查询……各干各的),而日志、事务、权限检查这些需求,是横着切过所有竖线的——这就是"横切关注点"(Cross-cutting Concern),也是 AOP 名字的由来。
AOP 的做法:
plain
第 1 步:业务方法保持干净,一行日志都不写
public void 转账() { 只写转账逻辑 }
第 2 步:把日志代码单独写在一个"切面类"里(只写一遍)
第 3 步:写一条"规则"告诉 Spring:
"所有 Service 里的方法,执行前后都套上那段日志代码"
第 4 步:Spring 在运行时用【动态代理】悄悄把日志织进去
你调用 转账(),实际执行的是:
日志开始 → 转账逻辑 → 日志结束
而你的 转账() 源码里干干净净
用一个类比彻底理解:
你开了一家煎饼摊,100 个煎饼(100 个业务方法)。老板说"每个煎饼都加个鸡蛋"(日志)。
笨办法(没有 AOP):摊每个饼的时候都手动打个蛋,动作重复 100 次。 AOP 办法:你跟煎饼机(Spring 容器)说"凡是出摊的饼,一律加蛋",机器自动给每个饼加蛋。你摊饼的动作完全没变,改需求时只跟机器说一句就行。
9.3 AOP 五个核心术语(用人话翻译)
表格
| 术语 | 英文 | 大白话 |
|---|---|---|
| 切面 | Aspect | 那个"专门放通用代码的类",比如日志切面类 |
| 通知 | Advice | 通用代码本身 + "什么时候执行"(之前?之后?) |
| 切点 | Pointcut | "规则":哪些方法要被加鸡蛋(匹配表达式) |
| 连接点 | JoinPoint | 程序运行中能被拦截的位置(某个具体方法的执行) |
| 织入 | Weaving | 把通知"套"到目标方法上的动作(Spring 用动态代理自动完成) |
通知(Advice)按执行时机分五种:
表格
| 注解 | 时机 |
|---|---|
@Before |
目标方法执行前 |
@After |
目标方法执行后(不管是否异常) |
@AfterReturning |
目标方法正常返回后(能拿到返回值) |
@AfterThrowing |
目标方法抛异常后(能拿到异常对象) |
@Around |
前后全包,最强,能控制目标方法是否执行 |
9.4 手写一个日志切面(完整可运行)
第 1 步:加依赖(AOP 需要额外的包)
xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
第 2 步:业务类(注意:干干净净,没有任何日志)
java
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public void createOrder(String item, int count) {
System.out.println(" >> 业务执行:创建订单,商品=" + item + ",数量=" + count);
}
public int calcPrice(int count) {
return count * 100; // 假设单价 100
}
}
第 3 步:切面类(日志只写这一次)
java
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/*
@Aspect:声明"我是一个切面类"
@Component:同时也要是个 Bean,让容器管理
*/
@Aspect
@Component
public class LogAspect {
/*
@Before:在目标方法执行【之前】运行本方法
括号里的表达式就是"切点",拆解:
execution( 修饰符可省 返回值 包名.类名.方法名(参数) )
* 任意返回值
com.example.service.. 该包及其子包(两个点表示含子包)
*.*(..) 任意类的任意方法、任意参数
合起来:service 包下所有类的所有方法,都逃不过
*/
@Before("execution(* com.example.service..*.*(..))")
public void logBefore() {
System.out.println("【日志】方法即将执行...");
}
@After("execution(* com.example.service..*.*(..))")
public void logAfter() {
System.out.println("【日志】方法执行完毕。");
}
}
第 4 步:配置类开启 AOP
java
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy // 关键开关:告诉 Spring 启用 AOP 自动代理
public class AppConfig {
}
第 5 步:运行
java
public class Main {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
orderService.createOrder("机械键盘", 2);
}
}
输出:
plain
【日志】方法即将执行...
>> 业务执行:创建订单,商品=机械键盘,数量=2
【日志】方法执行完毕。
OrderService 的源码里一个日志字都没写,日志却自动出现了。改日志格式?只改 LogAspect 一个类。这就是 AOP。
9.5 进阶:拿到方法的参数和返回值
切面不仅能"旁听",还能拿到目标方法的详细信息:
java
import org.aspectj.lang.JoinPoint;
@Before("execution(* com.example.service..*.*(..))")
public void logBefore(JoinPoint jp) {
// JoinPoint = "连接点",封装了被拦截方法的一切信息
String methodName = jp.getSignature().getName(); // 方法名
Object[] args = jp.getArgs(); // 传入的实参
System.out.println("【日志】即将调用 " + methodName
+ ",参数=" + java.util.Arrays.toString(args));
}
// AfterReturning 能拿到返回值
@AfterReturning(
pointcut = "execution(* com.example.service..*.*(..))",
returning = "result") // returning 的名字要和参数名一致
public void logReturn(JoinPoint jp, Object result) {
System.out.println("【日志】" + jp.getSignature().getName()
+ " 返回了:" + result);
}
调用
calcPrice(2) 的输出:plain
【日志】即将调用 calcPrice,参数=[2]
【日志】方法执行完毕。
【日志】calcPrice 返回了:200
9.6 最强的 @Around:方法执行全包
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@Around("execution(* com.example.service..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
// pjp.proceed() = "放行",让目标方法真正执行
// 不写这行,目标方法就被你"拦截死"了(可做权限拦截)
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
System.out.println("【计时】" + pjp.getSignature().getName()
+ " 耗时 " + cost + " ms");
return result; // 把目标方法的返回值原样交回去
}
一个 @Around 就实现了"方法耗时统计"——把它放到你的项目里,所有 Service 方法的性能监控就有了,业务代码零改动。
9.7 底层原理一句话:动态代理
Spring 怎么做到"源码不改、功能增强"?答案是代理模式:
plain
你以为拿到的:OrderService 本体
实际拿到的: Spring 生成的【代理对象】
代理对象.method() {
日志前置();
本体.method(); // 转发给真正的对象
日志后置();
}
-
目标类有接口 → 用 JDK 动态代理
-
目标类没接口 → 用 CGLIB(生成目标类的子类做代理)
这就是为什么从容器 getBean 出来的对象可能和你写的类"长得不一样"——它是穿了个马甲的代理。
9.8 AOP 在真实项目中的用途
表格
| 用途 | 说明 |
|---|---|
| 日志记录 | 统一记录方法入参、出参 |
| 声明式事务 | 加个 @Transactional 自动开/提交/回滚事务(下一章主角) |
| 权限校验 | 方法执行前检查用户有没有权限 |
| 性能监控 | 统计每个方法耗时 |
| 统一异常处理 | 集中捕获处理异常 |
| 接口限流/防重 | 拦截重复提交 |
第十章:Spring 操作数据库(JdbcTemplate)
原生 JDBC 的痛点:获取连接、创建 Statement、处理异常、关闭资源……查一条数据要写 30 行模板代码。Spring 的 JdbcTemplate 把这些模板代码全包了。
10.1 准备工作
建库建表(MySQL):
sql
CREATE DATABASE spring_demo DEFAULT CHARSET utf8mb4;
USE spring_demo;
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
age INT
);
INSERT INTO user(username, age) VALUES ('张三', 25), ('李四', 30);
pom.xml 加依赖:
xml
<!-- Spring JDBC 模块:包含 JdbcTemplate -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
10.2 用配置类装配数据源和 JdbcTemplate
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
@ComponentScan("com.example")
public class AppConfig {
/*
@Bean 注解:告诉容器"把本方法的返回值也收进容器管理"
用于管理第三方类(DataSource 是别人写的,没法在它类上加 @Component)
*/
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_demo?useSSL=false&serverTimezone=UTC");
ds.setUsername("root");
ds.setPassword("你的密码");
return ds;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
// 参数上的 DataSource 会被容器自动注入 —— @Bean 方法之间也能依赖
return new JdbcTemplate(dataSource);
}
}
10.3 Dao 层:用 JdbcTemplate 增删改查
java
package com.example.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserDao {
// 注入容器里配好的 JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
// ========== 增 ==========
public void add(String username, int age) {
// update 用于 INSERT/UPDATE/DELETE,返回影响行数
// SQL 里的 ? 是占位符,后面参数按顺序填入,防 SQL 注入
jdbcTemplate.update(
"INSERT INTO user(username, age) VALUES(?, ?)",
username, age);
}
// ========== 改 ==========
public void updateAge(int id, int age) {
jdbcTemplate.update("UPDATE user SET age=? WHERE id=?", age, id);
}
// ========== 删 ==========
public void delete(int id) {
jdbcTemplate.update("DELETE FROM user WHERE id=?", id);
}
// ========== 查单个 ==========
public User findById(int id) {
// queryForObject:查一行,并自动封装成 User 对象
// BeanPropertyRowMapper:按"列名 = 属性名"自动对应
return jdbcTemplate.queryForObject(
"SELECT id, username, age FROM user WHERE id=?",
new BeanPropertyRowMapper<>(User.class),
id);
}
// ========== 查列表 ==========
public List<User> findAll() {
return jdbcTemplate.query(
"SELECT id, username, age FROM user",
new BeanPropertyRowMapper<>(User.class));
}
// ========== 查聚合(返回单个值) ==========
public int count() {
// 查单个简单值:第二参数直接给类型
return jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM user", Integer.class);
}
}
实体类(普通的 JavaBean,字段名和表列名对应):
java
package com.example.dao;
public class User {
private int id;
private String username;
private int age;
// getter/setter 省略(必须有,RowMapper 靠 setter 填充数据)
// ... 加上 toString() 方便打印
}
测试:
java
public class Main {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
UserDao dao = ctx.getBean(UserDao.class);
dao.add("王五", 28);
dao.findAll().forEach(System.out::println);
System.out.println("总人数:" + dao.count());
}
}
对比一下:原生 JDBC 查一个列表要 30 多行(含 try-catch-finally 关资源),这里 2 行。连接管理、异常转换、资源关闭,JdbcTemplate 全干了。
第十一章:声明式事务 —— AOP 最经典的应用
11.1 事务是什么?一个转账场景
java
// 转账:A 扣 100,B 加 100 —— 两个操作必须"同生共死"
accountDao.minusMoney("A", 100); // 假设这行成功了
// 如果这里突然断电/抛异常...
accountDao.addMoney("B", 100); // 这行没执行,钱凭空消失了!
事务(Transaction)保证一组 SQL 要么全成功,要么全回滚,四个特性即 ACID(原子性、一致性、隔离性、持久性)。
11.2 手动管事务有多烦
java
public void transfer() {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 1. 关自动提交 = 开启事务
accountDao.minusMoney(conn, "A", 100);
accountDao.addMoney(conn, "B", 100);
conn.commit(); // 2. 都成功,提交
} catch (Exception e) {
conn.rollback(); // 3. 出异常,回滚
} finally {
conn.close(); // 4. 关连接
}
}
每个需要事务的方法都要写这套模板 —— 这不就是第九章说的"横切代码"吗?事务就是 AOP 最典型的应用。
11.3 Spring 声明式事务:一个注解搞定
"声明式"的意思是:你不写事务代码,只声明"这个方法需要事务",Spring 用 AOP 自动帮你开/提交/回滚。
第 1 步:配置事务管理器 + 开启注解事务
java
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan("com.example")
@EnableTransactionManagement // 开关:启用 @Transactional 注解
public class AppConfig {
@Bean
public DataSource dataSource() { /* ...同上一章... */ }
// 事务管理器:告诉 Spring 用 JDBC 的方式管事务
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
第 2 步:业务方法上加 @Transactional
java
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountDao accountDao;
/*
@Transactional:声明本方法在一个事务中执行
Spring 的 AOP 代理会自动做:
方法开始前:开启事务
方法正常结束:提交事务
方法抛异常:回滚事务
你的代码里只剩纯业务!
*/
@Transactional
public void transfer(String from, String to, int money) {
accountDao.minusMoney(from, money);
// int i = 1 / 0; // 取消注释制造异常,观察回滚效果
accountDao.addMoney(to, money);
}
}
第 3 步:验证回滚
先正常调用一次
transfer("A", "B", 100),查数据库:A 少了 100,B 多了 100。然后取消
1/0 那行的注释再调用:虽然 A 已经执行了扣款,但整个事务回滚,A 的钱恢复原样——一分钱没丢。11.4 @Transactional 常用属性
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为(见下)
isolation = Isolation.READ_COMMITTED, // 隔离级别:读已提交(防脏读,常用)
timeout = 5, // 超时秒数,超时自动回滚
readOnly = true, // 只读事务(纯查询方法加上,有优化)
rollbackFor = Exception.class // 指定哪些异常回滚(默认只对运行时异常回滚)
)
public List<User> findAll() { ... }
传播行为(propagation)——解决"方法调方法,事务怎么套":
表格
| 值 | 含义 |
|---|---|
REQUIRED(默认) |
有事务就加入,没有就新建 —— 99% 的场景用它 |
REQUIRES_NEW |
不管外面有没有,我都开新事务(独立提交,常用于"不管成败都要记日志") |
SUPPORTS |
有事务就用,没有也能跑 |
面试高频坑:@Transactional 在同一个类里"自己调自己"会失效(因为没经过代理对象),记住这句话即可,原理在第九章动态代理部分。
第十二章:Spring MVC 简介(通往 Spring Boot 的桥梁)
12.1 Spring MVC 是干什么的
Spring MVC 是 Spring 家族里的 Web 模块:接收 HTTP 请求 → 调用你的业务代码 → 返回响应(页面或 JSON)。它同样建立在 IoC 容器之上,Controller 也是一个 Bean。
工作流程(MVC = Model 模型 / View 视图 / Controller 控制器):
plain
浏览器请求 /user/list
│
▼
DispatcherServlet(前端控制器,总入口,统一收单)
│
▼ 根据 URL 找对应的处理方法
@Controller 里的某个 @RequestMapping 方法
│
▼ 调用 Service → Dao → 数据库(就是你前面学的三层)
拿到结果,返回 JSON / 页面
12.2 最小示例
java
package com.example.controller;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller // Web 层的 Bean
public class UserController {
@Autowired
private UserService userService; // Controller 也是 Bean,照样注入 Service
/*
@RequestMapping:把 URL "/user/register" 映射到本方法
浏览器访问 http://localhost:8080/user/register?name=张三 时执行
*/
@RequestMapping("/user/register")
@ResponseBody // 返回值直接当响应体写回浏览器(不跳转页面)
public String register(String name) {
userService.register(name);
return "注册成功:" + name; // @ResponseBody 让这个字符串直接显示在浏览器
}
}
这里就不展开 web.xml、DispatcherServlet 的繁琐配置了——因为那正是 Spring Boot 要消灭的东西。你只需要建立这个认知:Spring Boot = Spring + Spring MVC + 自动配置 + 内嵌 Tomcat。 你上面看到的@Controller、@RequestMapping、@Autowired,在 Spring Boot 里一模一样地写,区别只是 Boot 帮你把容器、Tomcat、配置全部自动装好,main方法一跑项目就启动了。
12.3 学到这里,你已经具备了学 Spring Boot 的全部前提
表格
| Spring Boot 里你会遇到 | 对应本教程章节 |
|---|---|
@SpringBootApplication |
第七章 @Configuration + @ComponentScan 的合体 |
@Autowired 到处注入 |
第四章、第七章 |
application.yml 配置 |
第七章 @Value + @PropertySource 的升级版 |
@Transactional |
第十一章 |
@Controller / @Service 分层 |
第七章 |
| AOP 日志/权限 | 第九章 |
第十三章:常见面试题与学习路线
13.1 十个高频面试题(自检清单)
-
什么是 IoC? 对象创建和管理的控制权从程序代码反转给 Spring 容器;容器通过反射 + 大 Map 管理 Bean。
-
IoC 和 DI 的关系? IoC 是设计思想(控制权反转),DI 是实现手段(通过构造/setter/注解把依赖注入)。
-
@Autowired 的装配规则? 默认按类型 byType;同类型有多个时按字段名 byName;还冲突用 @Qualifier 指定。
-
Bean 的默认作用域? singleton,容器级单例;prototype 每次新建。
-
Bean 生命周期? 实例化 → 属性填充(DI)→ 初始化(@PostConstruct)→ 使用 → 销毁(@PreDestroy)。
-
AOP 是什么?解决了什么? 把日志、事务等横切关注点从业务代码中剥离,通过动态代理统一织入,解决代码重复与污染。
-
AOP 底层原理? 动态代理:有接口用 JDK 代理,无接口用 CGLIB 生成子类代理。
-
@Transactional 失效的常见原因? ① 同类内部自调用(绕过代理)② 方法不是 public ③ 异常被 catch 吞掉没抛出。
-
@Component/@Service/@Repository/@Controller 区别? 功能相同,语义分层不同,@Repository 额外做异常翻译。
-
构造注入和 setter 注入哪个好? 官方推荐构造注入:依赖不可变(final)、对象创建即完整、便于测试。
13.2 建议的学习路线
plain
✅ Java SE(集合、反射、注解) ← 你已有
✅ 本教程:Spring Framework ← 现在在这里
⬇
Spring Boot(自动配置、Web 开发、配置文件)
⬇
MyBatis / MyBatis-Plus(持久层框架,替代手写 JdbcTemplate)
⬇
Redis、消息队列(RabbitMQ/Kafka)、Spring Security
⬇
Spring Cloud(微服务:注册中心、网关、远程调用)
13.3 给初学者的三条忠告
-
不要跳过 XML 配置直接学注解。XML 让"注入"看得见摸得着,理解了它再看注解,会有一种"原来注解只是把配置搬了个家"的通透感。
-
每个例子都亲手敲一遍,并故意改坏它。比如把 Bean 的 id 写错、让 @Autowired 找不到实现——看报错信息是最好的老师。
-
学 AOP 时死磕"动态代理"四个字。想明白"你拿到的不是本体,是穿了马甲的代理",@Transactional 失效、方法拦截这些高级问题全都迎刃而解。
教程到此结束。建议上机顺序:第五章跑通 → 第六章理解注入 → 第七章换注解重写 → 第九章 AOP 日志 → 第十章连数据库 → 第十一章转账事务。全部跟完,大约 2~3 天,之后直接开 Spring Boot 会非常顺。
浙公网安备 33010602011771号