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 的查找规则(重要)

容器按以下顺序找"注射什么进来":
  1. 先按类型找:容器里有几个 UserDao 类型的 Bean?
  2. 只有 1 个 → 直接注入,完美。
  3. 有多个(比如同时存在 UserDaoMysql 和 UserDaoOracle)→ 再按字段名当 Bean 名找:
java
 
@Autowired
private UserDao userDaoOracle;   // 字段名 = Bean 名,容器按名字精确定位
  1. 还搞不定,用 @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 遍
三个致命问题:
  1. 重复:同样的日志代码复制 100 份;
  2. 污染:业务方法里掺着不相干的代码,读代码的人要在一堆日志里找业务;
  3. 改不动:老板改口说"日志前面加个时间戳",你要改 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 十个高频面试题(自检清单)

  1. 什么是 IoC? 对象创建和管理的控制权从程序代码反转给 Spring 容器;容器通过反射 + 大 Map 管理 Bean。
  2. IoC 和 DI 的关系? IoC 是设计思想(控制权反转),DI 是实现手段(通过构造/setter/注解把依赖注入)。
  3. @Autowired 的装配规则? 默认按类型 byType;同类型有多个时按字段名 byName;还冲突用 @Qualifier 指定。
  4. Bean 的默认作用域? singleton,容器级单例;prototype 每次新建。
  5. Bean 生命周期? 实例化 → 属性填充(DI)→ 初始化(@PostConstruct)→ 使用 → 销毁(@PreDestroy)。
  6. AOP 是什么?解决了什么? 把日志、事务等横切关注点从业务代码中剥离,通过动态代理统一织入,解决代码重复与污染。
  7. AOP 底层原理? 动态代理:有接口用 JDK 代理,无接口用 CGLIB 生成子类代理。
  8. @Transactional 失效的常见原因? ① 同类内部自调用(绕过代理)② 方法不是 public ③ 异常被 catch 吞掉没抛出。
  9. @Component/@Service/@Repository/@Controller 区别? 功能相同,语义分层不同,@Repository 额外做异常翻译。
  10. 构造注入和 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 给初学者的三条忠告

  1. 不要跳过 XML 配置直接学注解。XML 让"注入"看得见摸得着,理解了它再看注解,会有一种"原来注解只是把配置搬了个家"的通透感。
  2. 每个例子都亲手敲一遍,并故意改坏它。比如把 Bean 的 id 写错、让 @Autowired 找不到实现——看报错信息是最好的老师。
  3. 学 AOP 时死磕"动态代理"四个字。想明白"你拿到的不是本体,是穿了马甲的代理",@Transactional 失效、方法拦截这些高级问题全都迎刃而解。

教程到此结束。建议上机顺序:第五章跑通 → 第六章理解注入 → 第七章换注解重写 → 第九章 AOP 日志 → 第十章连数据库 → 第十一章转账事务。全部跟完,大约 2~3 天,之后直接开 Spring Boot 会非常顺。
posted @ 2026-08-06 16:26  鬼门元歌  阅读(13)  评论(0)    收藏  举报