Spring是什么

Spring 是一个轻量级 Java 开发框架,核心思想是 IoC 和 AOP。
IoC 用来管理对象的创建和依赖关系,降低耦合;AOP 用来把事务、日志、权限这些公共逻辑从业务代码中抽离出来。
在此基础上,Spring 又提供了 MVC、事务管理、整合数据库等能力,所以它成了 Java 后端开发的核心框架。

没有 Spring:

1
2
UserService userService = new UserService();
OrderService orderService = new OrderService(userService);

有 Spring:

1
2
3
4
5
6
7
8
9
10
@Service
public class UserService {
}

@Service
public class OrderService {

@Autowired
private UserService userService;
}

区别:

  • 以前你自己创建对象
  • 现在 Spring 帮你创建对象
  • 以前你自己维护依赖关系
  • 现在 Spring 帮你注入依赖

IoC 和 DI

IoC:控制反转

DI:依赖注入

IoC 是控制反转思想,DI 是 IoC 的实现方式之一。

通过 IoC 和 DI,Spring 可以降低对象之间的耦合,提高系统的可维护性和扩展性。

什么叫“控制反转”

先看不用 Spring 的情况。

假设有这两个类:

1
2
3
4
5
public class UserService {
}
public class UserController {
private UserService userService = new UserService();
}

这里谁在“控制”对象的创建?

答案是:UserController 自己控制 UserService 的创建。

也就是说:

  • 需要谁,就自己 new
  • 对象什么时候创建、怎么创建,都由程序员自己决定

控制权在对象自己手里

如果用了 Spring:

1
2
3
4
5
6
@RestController
public class UserController {

@Autowired
private UserService userService;
}

这时候你没有自己 new UserService()

而是 Spring 容器在启动时帮你创建好 UserService,然后再放进 UserController 里。

也就是说:

  • 以前:对象自己创建依赖对象
  • 现在:对象不再自己创建,交给 Spring 容器来创建和管理

这就叫 控制反转(IoC)

一句话理解

原来对象的控制权在程序员手里,现在反转给 Spring 容器了。

DI 是什么

DI 是 Dependency Injection,依赖注入。

意思是:

一个对象依赖另一个对象时,不自己创建,而是由容器把依赖对象注入进来。

比如:

1
UserController` 依赖 `UserService

Spring 把 UserService 塞给 UserController

这就叫 依赖注入

DI 常见注入方式

依赖注入有哪些方式?

1)构造器注入

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {
}
@RestController
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}
}

2)Setter 注入

1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController {

private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}

3)字段注入

1
2
3
4
5
6
@RestController
public class UserController {

@Autowired
private UserService userService;
}

其中构造器注入最推荐,因为依赖关系清晰,适合不可变对象,也方便单元测试。
字段注入虽然写法简洁,但不利于测试和维护。

Bean

Bean 就是交给 Spring 容器管理的对象。

Bean 和普通对象的区别

Bean 本质上也是对象。
只是它比普通对象多了一层身份:

  • 普通对象:你自己 new 出来的
  • Bean:Spring 创建、保存、管理的对象

比如这个类:

1
2
3
@Service
public class UserService {
}

Spring 扫描到 @Service 后,会把 UserService 创建出来放进容器里。

这个 UserService 对象就是 Bean。

创建Bean

方式一:用注解声明

最常见。

1
2
3
@Service
public class UserService {
}

或者:

1
2
3
@Component
public class UserService {
}

Spring 扫描到这些注解后,就会把类实例化成 Bean。

常见注解有:

  • @Component
  • @Service
  • @Repository
  • @Controller
  • @RestController

这些本质上都和 Bean 有关。

方式二:用 @Bean 方法注册

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public UserService userService() {
return new UserService();
}
}

这里 userService() 返回的对象,也会被放进 Spring 容器。

这也是 Bean。

这个方式常用于:

  • 第三方类不能加注解
  • 手动控制创建逻辑

方式三:XML 配置

1
<bean id="userService" class="com.example.UserService"/>

Bean 在容器里长什么样

可以把 Spring 容器想象成一个大仓库。

仓库里存很多对象:

  • userService
  • orderService
  • userController
  • dataSource

这些对象都带着自己的信息,比如:

  • 名字
  • 类型
  • 作用域
  • 是否单例
  • 初始化方法
  • 销毁方法
  • 依赖关系

Spring 就根据这些信息来管理 Bean。

所以 Bean 不只是“一个对象”,它还有一套元信息。

Bean = 对象 + 被 Spring 管理的配置信息

Bean 有什么好处

把对象变成 Bean 后,Spring 就能帮我们做很多事。

1)统一创建

你不用自己 new

2)统一注入依赖

配合 @Autowired

3)统一管理生命周期

比如初始化、销毁

4)统一扩展功能

比如事务、AOP、代理

所以 Bean 是 Spring 一切能力的基础。

很多能力不是直接加在“类”上,而是加在“Bean”上。

BeanFactory 和 ApplicationContext

BeanFactory 是 Spring 最底层的容器接口。
ApplicationContext 是 BeanFactory 的高级版本。

BeanFactory 是什么

BeanFactory 是 Spring 最核心、最基础的容器接口。

它最主要的职责就是:

  • 管理 Bean
  • 按需获取 Bean

ApplicationContext 是什么

ApplicationContextBeanFactory 的子接口。

它不仅有 BeanFactory 的所有能力,还额外增强了很多功能。

对比

特性 BeanFactory ApplicationContext
加载策略 懒加载(访问时才创建) 预加载(启动时即创建)
内存占用 较低(适合移动端或轻量级应用) 相对较高(预先创建了大量对象)
国际化 (i18n) 不支持 支持(通过 MessageSource)
事件机制 不支持 支持(ApplicationEvent)
注解支持 需要手动注册后置处理器 自动注册(如 @Autowired 等)

为什么 ApplicationContext 更常用?

虽然 BeanFactory 听起来更省内存,但在现代开发中,我们几乎 99% 的场景都会直接使用 ApplicationContext。原因如下:

  • 早发现,早治疗: 由于 ApplicationContext 在启动时就实例化所有单例,如果你的配置写错了(比如循环依赖或者类名写错),程序在启动阶段就会报错,而不是等到运行到一半访问该对象时才崩溃。

  • 功能全家桶: 它自带对 AOP(面向切面编程)的无缝支持、资源加载(ResourceLoader)、以及与 Web 环境的完美集成。

Bean的生命周期

实例化 → 依赖注入 → 初始化 → 使用 → 销毁

在 Spring 看来,Bean 在真正创建前,先得有一份“说明书”。

这份说明书里会写:

  • 这个 Bean 的名字是什么
  • 它的类型是什么
  • 它是单例还是多例
  • 它依赖谁
  • 它有没有初始化方法
  • 它有没有销毁方法

Spring 把这份“说明书”叫:

BeanDefinition

所以 Bean 创建的前提是:

Spring 先拿到 BeanDefinition,再根据定义去创建 Bean。

第一步:实例化 Bean

Spring 先根据 Bean 定义,把对象创建出来。

比如:

1
2
3
@Service
public class UserService {
}

Spring 容器启动时,会先想办法把 UserService 对象创建出来。

第二步:依赖注入

比如:

1
2
3
4
5
6
@Service
public class OrderService {

@Autowired
private UserService userService;
}

Spring 创建完 OrderService 后,会继续把它依赖的 UserService 注入进去。

这一步后,对象和它需要的依赖才真正连起来。

第三步:初始化

对象创建了,依赖也注入了,但有时候还需要做一些“准备动作”。

比如:

  • 建立连接
  • 读取配置
  • 初始化缓存
  • 做一些校验

Spring 就提供了初始化阶段。

常见方式有:

方式 1:@PostConstruct

1
2
3
4
5
6
7
8
@Service
public class UserService {

@PostConstruct
public void init() {
System.out.println("初始化 UserService");
}
}

方式 2:实现 InitializingBean

1
2
3
4
5
6
7
8
@Service
public class UserService implements InitializingBean {

@Override
public void afterPropertiesSet() {
System.out.println("初始化 UserService");
}
}

方式 3:指定 init-method

1
2
3
<bean id="userService" class="com.example.UserService" init-method="init"/>
//Spring 创建好 UserService 这个 Bean 后,会再调用它的 init() 方法。

第四步:使用

初始化完成后,这个 Bean 就进入可用状态了。

比如:

  • 被 Controller 调用
  • 被 Service 使用
  • 被事务代理增强
  • 被 AOP 拦截

这时候它就开始正常参与业务运行。

这个阶段其实没什么复杂的,就是:

Bean 已经准备好,可以被容器和业务代码使用了。

第五步:销毁

当容器关闭时,Spring 会把 Bean 销毁。

比如项目停掉时,某些资源需要释放:

  • 关闭数据库连接
  • 关闭线程池
  • 清理缓存
  • 释放文件句柄

Spring 会在销毁前调用一些回调方法。

常见方式有:

方式 1:@PreDestroy

1
2
3
4
5
6
7
8
@Service
public class UserService {

@PreDestroy
public void destroy() {
System.out.println("销毁 UserService");
}
}

方式 2:实现 DisposableBean

1
2
3
4
5
6
7
8
@Service
public class UserService implements DisposableBean {

@Override
public void destroy() {
System.out.println("销毁 UserService");
}
}

方式 3:指定 destroy-method

1
<bean id="userService" class="com.example.UserService" destroy-method="destroy"/>

什么是 Aware 回调

这是中间常见的一步。

如果一个 Bean 实现了某些 Aware 接口,Spring 会把一些容器信息“告诉它”。

比如:

  • BeanNameAware
  • BeanFactoryAware
  • ApplicationContextAware

例子:

1
2
3
4
5
6
7
8
@Service
public class UserService implements BeanNameAware {

@Override
public void setBeanName(String name) {
System.out.println("Bean name: " + name);
}
}

意思是 Spring 会在生命周期过程中,把当前 Bean 的名字传给它。

什么是 BeanPostProcessor

这个也很高频。

它的作用是:

允许 Spring 在 Bean 初始化前后,对 Bean 做额外处理。

比如 AOP 代理对象,很多时候就是在这个阶段织入的。

简单理解:

  • 初始化前,可以加工一下
  • 初始化后,也可以加工一下

单例 Bean 和 prototype Bean

singleton 是整个 Spring 容器里通常只有一个 Bean 实例。

1
2
3
4
@Service
@Scope("singleton")
public class UserService {
}
1
2
3
UserService a = context.getBean(UserService.class);
UserService b = context.getBean(UserService.class);
System.out.println(a == b); //true

prototype 是每次获取 Bean 时,都会创建一个新的实例。

1
2
3
4
@Service
@Scope("prototype")
public class UserService {
}
1
2
3
UserService a = context.getBean(UserService.class);
UserService b = context.getBean(UserService.class);
System.out.println(a == b); //false

Spring 默认是 singleton

因为大多数业务类其实不需要创建很多份。

singleton 适合什么场景

无状态 Bean

所谓无状态,就是类里不保存“每次请求独有的数据”。

prototype 适合什么场景

适合:

一个 Bean 需要保存独立状态

singleton Bean 生命周期

Spring 会比较完整地管理它:

  • 创建
  • 依赖注入
  • 初始化
  • 使用
  • 销毁

当容器关闭时,Spring 会调用它的销毁方法。


prototype Bean 生命周期

Spring 通常只负责前半段:

  • 创建
  • 依赖注入
  • 初始化

不负责完整销毁

也就是说:

拿到 prototype Bean 后,后面它什么时候不用了、怎么释放资源,Spring 通常不管。

创建时机的区别


singleton

一般在容器启动时,默认会提前创建单例 Bean。

所以项目一启动,它们很多就已经准备好了。


prototype

不会在容器启动时统一创建。
通常是你每次调用 getBean() 或每次真正需要它时,才创建新的对象。

对比

对比点 singleton prototype
实例数量 容器中通常一个 每次获取都新建
是否默认
获取 Bean 时 多次拿到同一个对象 多次拿到不同对象
创建时机 常见是容器启动时创建 获取时创建
销毁管理 Spring 会管理 Spring 通常不负责销毁
适合场景 无状态共享对象 有状态独立对象
线程安全 要特别注意 相对更少共享问题

AOP

面向切面编程

它本质上就是:

把那些和业务无关、但很多地方都要用的公共逻辑,抽出来统一处理。

比如:

  • 日志
  • 事务
  • 权限校验
  • 性能统计
  • 异常处理

这些东西不是某个业务独有的,而是“横着切”很多业务方法。

AOP 解决了什么问题

1)把公共逻辑和业务逻辑分离

业务代码只管业务。

2)减少重复代码

日志、事务、鉴权不用每个方法都手写。

所以 AOP 的价值就是:

解耦 + 复用

AOP的几个概念

切面(Aspect)

切面就是:

封装公共逻辑的类

比如日志切面、事务切面、权限切面。

1
2
3
4
@Aspect
@Component
public class LogAspect {
}

这个类就是一个切面。

连接点(JoinPoint)

连接点就是:

程序运行过程中,可以被拦截的位置

在 Spring AOP 里,最常见的连接点其实就是:

方法执行

哪些方法可以被增强,那些方法执行点就是连接点。

切点(Pointcut)

切点就是:

到底要拦截哪些连接点

也就是从所有方法里,挑出要增强的那一部分。

比如:

1
execution(* com.example.service..*(..))

意思是:

拦截 service 包下所有类的所有方法。

这就是切点表达式。

通知(Advice)

通知就是:

在目标方法的什么时机,执行什么增强逻辑

常见通知有:

  • @Before:方法执行前
  • @After:方法执行后
  • @AfterReturning:方法正常返回后
  • @AfterThrowing:方法抛异常后
  • @Around:环绕通知,功能最强

比如:

1
2
3
4
@Before("execution(* com.example.service..*(..))")
public void before() {
System.out.println("前置通知");
}

织入(waving)

把切面应用到目标对象,从而创建代理对象的过程。

目标对象(Target)

目标对象就是:

原本真正执行业务逻辑的那个对象

代理对象(Proxy)

Spring AOP 真正运行时,不一定直接用目标对象,而是会创建一个代理对象

AOP 框架创建的对象,它包含了目标对象的所有方法,并织入了切面逻辑。

AOP 是怎么做到“无侵入增强”的?

动态代理

Spring AOP 不是直接改你的源码,
而是给你的 Bean 包一层代理对象。

调用流程大概变成:

调用者 → 代理对象 → 目标对象

代理对象负责插入日志、事务、权限这些逻辑。

1
orderService.createOrder();

如果这个方法被 AOP 增强了,实际可能不是直接调 OrderService,而是:

  1. 先进入代理对象
  2. 执行前置通知
  3. 调用目标方法 createOrder()
  4. 执行后置通知
  5. 返回结果

OOP:面向对象编程

JDK 和 CGLIB 动态代理

JDK 动态代理是什么

JDK 动态代理是 Java 自带的代理机制。
它的特点是:

它要求目标对象必须实现接口。

比如:

1
2
3
4
5
6
7
8
9
10
public interface UserService {
void login();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void login() {
System.out.println("登录");
}
}

这时候 JDK 就可以根据接口 UserService,动态生成一个代理对象。

这个代理对象:

  • 看起来也是 UserService
  • 实际内部会拦截方法调用
  • 再把调用转发给目标对象

CGLIB 动态代理是什么

CGLIB 是一种基于字节码生成的代理方式。

它的特点是:

不要求目标类实现接口。

它是怎么做的:

直接继承目标类,然后重写目标方法,在重写的方法里加增强逻辑。

比如:

1
2
3
4
5
public class UserService {
public void login() {
System.out.println("登录");
}
}

CGLIB 会生成一个类似这样的子类:

1
2
3
4
5
6
7
8
public class UserServiceProxy extends UserService {
@Override
public void login() {
System.out.println("前置增强");
super.login();
System.out.println("后置增强");
}
}

当然这不是它真实源码,只是帮助理解。

区别

特性 JDK 动态代理 CGLIB 代理
实现原理 基于接口(反射机制) 基于继承(底层字节码技术)
代理对象关系 代理类与目标类是兄弟关系 代理类是目标类的子类
限制条件 目标类必须实现至少一个接口 目标类/方法不能被 final 修饰
底层库 Java 内置(java.lang.reflect.Proxy 第三方库(net.sf.cglib.proxy
执行效率 在新版 JDK 中效率非常高 代理创建慢,但执行方法时效率略高

Spring 更喜欢先用 JDK 动态代理

因为 JDK 动态代理是 Java 原生支持的,比较标准,也不需要额外通过继承去生成子类

Spring 事务

事务就是一组操作作为一个整体执行,要么全部成功,要么全部失败回滚。

而Spring 事务就是把事务控制从业务代码里抽出来,交给 Spring 统一管理。

用法

1
2
3
4
5
6
7
8
9
@Service
public class AccountService {

@Transactional
public void transfer() {
System.out.println("扣减 A 账户余额");
System.out.println("增加 B 账户余额");
}
}

虽然没手动写:

  • begin
  • commit
  • rollback

但 Spring 会帮你处理。

原理

AOP + 代理对象

调用:

1
accountService.transfer();

实际可能不是直接进 transfer(),而是:

  1. 先进入代理对象
  2. 代理对象先开启事务
  3. 再调用目标对象的 transfer()
  4. 如果成功,提交事务
  5. 如果抛异常,回滚事务

可以脑补成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public void transfer() {
// 1. 前置增强 (Before Advice): 开启事务环境
// 告诉数据库:接下来的操作是一个整体(Atomic)
beginTransaction();

try {
// 2. 核心业务逻辑 (Join Point / Target Method):
// 调用原始目标对象(Target)的转账方法。

target.transfer();

// 3. 返回增强 (After Returning Advice): 提交事务
// 如果上面一行没报错,就把所有数据库修改永久保存
commit();

} catch (Exception e) {
// 4. 异常增强 (After Throwing Advice): 事务回滚
// 只要中间任何一步出错了(比如余额不足、网络超时),
// 立即撤销刚才在该事务中执行的所有操作,保证数据一致性。
rollback();

// 5. 关键动作:将异常原样抛出
// 必须要抛出,否则调用方会以为转账成功了,且 Spring 的上一层容器也无法感知失败。
throw e;
}
// 6. 后置增强 (After Advice / Finally):
// 这里可以放一些无论成败都要执行的逻辑,比如释放数据库连接(类似 finally 块)
}

声明式事务

Spring 事务管理有两种思路:

1)编程式事务

自己写事务控制代码。

比如手动写:

1
2
3
4
5
6
7
try {
// 开启事务
// 执行业务
// 提交
} catch (Exception e) {
// 回滚
}

2)声明式事务

只声明“这个方法需要事务”,具体怎么开、怎么提交、怎么回滚,交给 Spring。

最典型就是:

1
2
3
@Transactional
public void transfer() {
}

现在项目里最常说的 Spring 事务,通常指的就是:

声明式事务

Spring 事务默认什么时候回滚

默认情况下:

Spring 只对运行时异常 RuntimeExceptionError 回滚。

比如:

1
throw new RuntimeException("出错了");

会回滚。

但如果你抛的是普通受检异常 Exception,默认不一定回滚。

例如:

1
throw new Exception("出错了");

默认可能不回滚。

如果你想让它也回滚,要这样写:

1
@Transactional(rollbackFor = Exception.class)

事务传播行为

事务传播行为,研究的是:

当一个带事务的方法,去调用另一个也带事务的方法时,事务到底该怎么传。

  • REQUIRED(默认,最常用)

    有事务就加入,没有事务就自己新建。

    1
    @Transactional(propagation = Propagation.REQUIRED)

    A 调用 B,如果 A 报错,B 回滚;如果 B 报错,A 也得跟着回滚。它们是在同一个连接(Connection)里跑的。


    REQUIRES_NEW`

    不管外面有没有事务,我都自己新开一个事务。

    1
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    • 外面有事务:先把外面的挂起
    • 我自己单独开一个新事务
    • 我执行完了,外面的再继续

    大白话“各过各的。” B 的成功与否不影响 A。

    典型场景写日志。即使转账(A)失败回滚了,记录操作日志(B)的动作也必须成功。


    SUPPORTS

    有事务就加入,没有事务就不用事务。

    1
    @Transactional(propagation = Propagation.SUPPORTS)
    • 外面有事务:那我顺便加入
    • 外面没事务:那我就直接普通执行

    NESTED

    有事务时就在当前事务里开一个嵌套事务,没有事务时通常按 REQUIRED 处理。

    1
    @Transactional(propagation = Propagation.NESTED)
    • 外层有事务:我在里面再套一层
    • 我可以局部回滚,不一定把整个大事务都一起干掉
    • 底层依赖保存点(savepoint)

    A 挂了,B 必挂。但 B 挂了,A 可以不挂

传播行为 含义
REQUIRED 有事务就加入,没有就新建
REQUIRES_NEW 总是新建新事务,挂起外部事务
SUPPORTS 有事务就加入,没有就不用事务
MANDATORY 必须在事务中,否则报错
NOT_SUPPORTED 以非事务方式运行,有事务就挂起
NEVER 不能在事务中运行,有事务就报错
NESTED 有事务就创建嵌套事务,没有就新建

@Transactional 为什么会失效

内部自调用 (Self-invocation): 同一个类中,方法 A 调用方法 B,而 B 上加了 @Transactional。此时 B 的事务会失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService {

public void methodA() {
methodB();
}

@Transactional
public void methodB() {
System.out.println("执行事务方法");
}

}
  • 原因:AOP 是通过代理对象(Proxy)拦截调用的。在类内部通过 this.B() 调用时,是原始对象在执行,绕过了代理对象,事务逻辑(切面)也就没机会执行。
  • 对策:将 B 移到另一个 Service,或者通过 AopContext.currentProxy() 获取当前代理对象再调用。

方法非 public 修饰: 如果方法是 privateprotecteddefault,事务通常会失效。

  • 原因:Spring 事务拦截器默认只检查 public 方法。此外,JDK 代理要求方法必须在接口中定义,而 CGLIB 代理通过继承实现,无法重写 private 方法。

还有可能是抛出的异常不对:spring事务只对 RuntimeExceptionError 回滚。

也可能Bean 没被 Spring 管理

只有 Spring 容器中的 Bean,Spring 才能给它生成事务代理。

Spring MVC

负责把浏览器请求,交给后端 Java 方法处理,再把结果返回给前端的一套机制。

DispatcherServlet
-> HandlerAdapter
-> 调用 Controller 方法
-> 方法内部业务逻辑执行
-> 拿到返回值

谁是 Spring MVC 的核心入口

DispatcherServlet

它是 Spring MVC 的前端控制器,几乎所有请求都会先到它这里。

所有请求先交给它,它再决定:

  • 该找谁处理
  • 怎么调用
  • 怎么返回

第一步:浏览器发送请求

比如浏览器访问:

1
GET /user/get?id=1

或者前端发送一个 POST 请求:

1
POST /user/save

这个 HTTP 请求会先被 Web 容器接收,比如 Tomcat。

然后再交给 Spring MVC 的 DispatcherServlet


第二步:DispatcherServlet 接收请求

DispatcherServlet 收到请求后,不会自己处理业务。

它的职责更像“总控台”,不是“干业务的人”。


第三步:通过 HandlerMapping 找到处理器

Spring MVC 要先知道:

这个请求该由哪个 Controller 的哪个方法处理。

这件事通常由HandlerMapping来完成。

比如有 Controller:

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/user")
public class UserController {

@GetMapping("/get")
public String getUser(Integer id) {
return "user:" + id;
}
}

当请求是:

1
/user/get?id=1

HandlerMapping 会把它匹配到:

1
UserController#getUser(Integer id)

HandlerMapping 的作用是根据请求路径,找到要执行的目标方法。


第四步:通过 HandlerAdapter 调用目标方法

找到方法后,还不能直接粗暴调,因为 Spring MVC 还要处理很多细节:

  • 参数绑定
  • 注解解析
  • 请求体转换
  • 返回值处理准备

所以 Spring MVC 不会自己直接硬调 Controller,而是通过:

HandlerAdapter来适配调用。

可以把它理解成:

真正负责“把请求转换成方法调用”的执行器。


第五步:参数绑定

比如你的 Controller 方法是:

1
2
3
4
@GetMapping("/get")
public String getUser(@RequestParam Integer id) {
return "user:" + id;
}

请求是:

1
/user/get?id=1

Spring MVC 会自动把请求参数里的:

1
id=1

绑定到方法参数:

1
Integer id

再比如:

1
2
3
4
@GetMapping("/user/{id}")
public String getUser(@PathVariable Integer id) {
return "user:" + id;
}

请求:

1
/user/1

Spring MVC 会把路径中的 1 绑定给 id

还有 POST JSON 请求:

1
2
3
4
@PostMapping("/save")
public String saveUser(@RequestBody User user) {
return "ok";
}

Spring MVC 会把请求体 JSON 转成 Java 对象。

所以这一阶段,本质上是:

把 HTTP 请求的数据,转换成 Controller 方法需要的参数。


第六步:调用 Controller 方法

参数准备好后,就真正执行 Controller 方法。

比如:

1
2
3
public String getUser(Integer id) {
return "user:" + id;
}

这一步才真正开始执行业务逻辑。

当然很多时候 Controller 还会再调用 Service:

1
2
3
public UserVO getUser(Integer id) {
return userService.getById(id);
}

第七步:处理返回值

Controller 方法执行完后,会返回结果。

比如可能返回:

  • 字符串
  • 对象
  • ModelAndView
  • JSON 数据
  • 视图名

Spring MVC 要根据返回值类型,决定怎么处理。

这部分一般由返回值处理器、视图解析器等机制参与。

Controller 返回的不是最终 HTTP 响应本体,Spring MVC 还要继续加工。


第八步:视图解析 or 直接返回 JSON

这里分两种大场景。

场景 1:传统 MVC 页面跳转

比如 Controller 返回:

1
return "userList";

这通常表示一个视图名。

Spring MVC 会通过:

ViewResolver(视图解析器)

把它解析成真正的页面路径,比如:

1
/WEB-INF/views/userList.jsp

然后再渲染页面返回给浏览器。


场景 2:前后端分离接口

如果你用的是:

1
@RestController

或者:

1
@ResponseBody

那 Controller 返回的对象通常不会走视图解析,而是直接转成 JSON 响应。

例如:

1
2
3
4
@GetMapping("/get")
public User getUser() {
return new User(1, "Tom");
}

Spring MVC 会把这个 User 对象转成 JSON 返回给前端。

HTTP 方法

方法 作用 例子
GET 查询 GET /user/1
POST 新增 POST /user
PUT 修改 PUT /user/1
DELETE 删除 DELETE /user/1

过滤器、拦截器、AOP

Filter 过滤器

属于 Servlet 规范
作用在 进入 Spring MVC 之前

Filter 是 Java Web 里的东西,不是 Spring 独有的。

它工作在请求刚进入 Web 容器、还没进 Spring MVC 的阶段。

可以把它理解成:最外层的一道网关。

比如请求从浏览器过来:

1
浏览器 -> Tomcat -> Filter -> DispatcherServlet -> Controller

Filter 常见用途

  • 统一字符编码处理
  • 登录校验
  • 跨域处理的一部分
  • 请求日志
  • 敏感词过滤

Interceptor 拦截器

属于 Spring MVC
作用在 Controller 前后

Interceptor 是 Spring MVC 的拦截器。

它是在请求已经进入 Spring MVC 之后,Controller 执行前后做增强。

链路可以理解成:

1
浏览器 -> Tomcat -> Filter -> DispatcherServlet -> Interceptor -> Controller

所以它比 Filter 更靠近业务。

Interceptor 常见用途

  • 登录权限校验
  • 接口访问日志
  • 统计接口耗时
  • 对 Controller 请求做前后处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("Controller 执行前");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("Controller 执行后");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("整个请求完成后");
}
}
  • preHandle:进 Controller 前
  • postHandle:Controller 后,视图渲染前
  • afterCompletion:整个请求结束后

AOP

属于 Spring 框架
作用在 方法层面

1
2
3
4
5
6
7
8
请求
-> Filter
-> DispatcherServlet
-> Interceptor.preHandle
-> Controller
-> Interceptor.postHandle
-> 视图渲染 / 返回响应
-> Interceptor.afterCompletion

如果 Controller 里再调 Service,而 Service 上有 AOP:

1
2
3
Controller
-> AOP 代理
-> Service 方法

对比

对比点 Filter Interceptor AOP
所属 Servlet 规范 Spring MVC Spring
作用位置 DispatcherServlet 之前 Controller 前后 方法调用前后
拦截对象 HTTP 请求 Controller 请求 Bean 方法
依赖 Spring 吗 不依赖 依赖 依赖
典型场景 编码、跨域、底层日志 登录校验、接口日志 事务、方法日志、权限、监控

@RequestParam@PathVariable@RequestBody

@RequestParam 是什么

它用来接收 URL 里的 查询参数,或者表单参数。

比如请求:

1
/user/get?id=1&name=Tom

Controller:

1
2
3
4
5
@GetMapping("/user/get")
public String getUser(@RequestParam Integer id,
@RequestParam String name) {
return id + ":" + name;
}

这里:

  • id=1
  • name=Tom

就是请求参数。

它最常见的场景:

GET 请求带参数

1
/user/get?id=1

表单提交

比如 application/x-www-form-urlencoded

特点

  • 取的是 ? 后面的参数
  • 可以指定参数名
  • 可以要求必传或非必传

例如:

1
@RequestParam(name = "id", required = false) Integer userId

@PathVariable 是什么

它用来接收 URL 路径中的动态部分。

比如请求:

1
/user/1

Controller:

1
2
3
4
@GetMapping("/user/{id}")
public String getUser(@PathVariable Integer id) {
return "user:" + id;
}

这里路径中的:

1
1

会被绑定到 id

它最常见的场景

RESTful 风格接口很常见:

  • /user/1
  • /order/1001
  • /product/88

这些路径里的变量就适合用 @PathVariable

特点

  • 值来自 URL 路径
  • 常用于资源定位
  • 更符合 RESTful 风格

@RequestBody 是什么

它用来接收 请求体中的数据,通常是 JSON。

比如前端发 POST 请求:

1
2
3
4
{
"id": 1,
"name": "Tom"
}

Controller:

1
2
3
4
@PostMapping("/user/save")
public String saveUser(@RequestBody User user) {
return user.getName();
}

Spring MVC 会把请求体中的 JSON 自动转成 User 对象。

它最常见的场景

前后端分离接口里非常常见,特别是:

  • POST
  • PUT
  • PATCH

提交 JSON 数据时,基本就会用 @RequestBody

对比

注解 数据来源 常见场景
@RequestParam 查询参数 / 表单参数 ?id=1
@PathVariable URL 路径变量 /user/1
@RequestBody 请求体 body JSON 提交

Spring Boot

Spring Boot自动配置

Spring Boot 自动配置的本质,就是 Spring Boot 启动时,自动把合适的配置类加载进 Spring 容器。

什么叫自动配置

Spring Boot 会根据当前项目里的依赖、环境、配置文件等条件,自动决定要不要创建某些 Bean。

比如:

  • 引入了 web 依赖
    → 自动配置 MVC 相关组件
  • 引入了数据源依赖并写了数据库配置
    → 自动配置 DataSource
  • 引入了 Jackson
    → 自动配置 JSON 转换器

所以自动配置的核心逻辑是:

按条件装配 Bean。

@SpringBootApplication

它本质上包含了三个关键注解:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

其中和自动配置最相关的,是:

@EnableAutoConfiguration

@EnableAutoConfiguration

它会触发 Spring Boot 去加载一批“自动配置类”。

这些自动配置类通常长这样:

  • DispatcherServletAutoConfiguration
  • DataSourceAutoConfiguration
  • JacksonAutoConfiguration
  • WebMvcAutoConfiguration

@EnableAutoConfiguration = 告诉 Spring Boot:去把那些自动配置类找出来,看看哪些该生效。

自动配置类从哪来

Spring Boot 会从依赖包里去找自动配置类。

老版本常见是:

1
META-INF/spring.factories

新版 Spring Boot 里更常见的是:

1
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

这些文件里会列出很多自动配置类的全限定名。

这些自动配置类通常来自依赖包中的自动配置清单文件,Spring Boot 会根据类路径、配置文件、Bean 是否存在等条件注解来判断哪些配置类需要生效。

约定大于配置,但允许你覆盖默认配置。

比如某个自动配置类里会写:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

意思是:

  • 你没自己配,我帮你配一个默认的
  • 你自己配了,我就不插手

pom.xml 加 starter
→ Maven 下载依赖
→ 类进入 classpath
@EnableAutoConfiguration 加载自动配置类
→ 条件匹配成功
→ 自动注册 Bean
→ 功能可用

自动装配 (Autowiring)

所属领域: Spring Framework (IoC 容器)

自动装配是解决 Bean 与 Bean 之间依赖注入(DI) 的问题。当一个组件需要另一个组件作为成员变量时,Spring 自动帮你把这个依赖找出来并注入进去。

  • 核心注解: @Autowired@Resource@Inject

  • 工作机制: 扫描 Spring 容器中已经存在的 Bean,根据类型(byType)或名称(byName)进行关联。

@SpringBootApplication

@SpringBootApplication 最经典的拆解是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

@SpringBootConfiguration

它本质上可以看作:

@Configuration 的一种 Spring Boot 版本声明

意思就是:

当前这个启动类本身也是一个配置类。

也就是说,这个类可以参与 Spring 的配置体系。

@EnableAutoConfiguration

开启 Spring Boot 自动配置。

@ComponentScan

它的作用是:

扫描当前包及其子包下的组件,并注册到 Spring 容器中。

比如有这些类:

1
2
3
4
5
6
7
@Service
public class UserService {
}

@RestController
public class UserController {
}

只要它们在启动类所在包或其子包下,@ComponentScan 就能扫到它们。

然后这些类就会变成 Bean。

它和 SpringApplication.run() 是什么关系

@SpringBootApplication`

负责声明:

  • 这是启动类
  • 要开启自动配置
  • 要扫描组件

SpringApplication.run(…)`

负责真正启动 Spring Boot 应用。

比如:

1
SpringApplication.run(DemoApplication.class, args);

它会去创建 Spring 容器、加载配置、启动内嵌服务器等。

为什么你自己定义的 Bean 能覆盖自动配置的 Bean

因为 Spring Boot 的自动配置通常都会配合 @ConditionalOnMissingBean

它的意思是:

只有当容器里还没有这个 Bean 时,自动配置才会生效。

例如某个自动配置类里可能有:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

这段话的意思就是:

  • 如果容器里没有 ObjectMapper
  • 那我就自动创建一个默认的 ObjectMapper

但如果已经写了:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MyConfig {

@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 自定义配置
return mapper;
}
}

那 Spring Boot 就会发现:

“容器里已经有 ObjectMapper 了。”

于是自动配置里的这个 Bean 就不会再创建。

循环依赖

循环依赖就是两个或多个对象在创建过程中,互相依赖对方。

1
2
3
4
5
6
@Component
public class AService {

@Autowired
private BService bService;
}
1
2
3
4
5
6
7
@Component
public class BService {

@Autowired
private AService aService;

}

有点像死锁

Spring 一定解决不了循环依赖吗

不是。

Spring 能解决一部分循环依赖,但不是所有循环依赖都能解决。

最常见的说法是:

Spring 默认主要能解决单例 Bean 的 setter/字段注入循环依赖。

但:

  • 构造器循环依赖,通常解决不了
  • prototype 循环依赖,通常也解决不了

字段/Setter 注入:先实例化,再注入,Spring 有机会提前暴露对象。
构造器注入:创建时就必须拿到完整依赖,所以循环依赖通常无解。

Spring 解决循环依赖靠什么

三级缓存

一级缓存:成品 Bean

二级缓存:提前暴露的半成品 Bean

三级缓存:生成早期 Bean 引用的工厂(ObjectFactory)

为什么不能只有两级缓存

两级缓存的问题:AOP 代理对象的提前暴露问题。

如果某个 Bean 后面会被 AOP 增强,那循环依赖场景里,别的 Bean 最终应该拿到的是:

代理对象

而不是原始对象。

三级缓存里的工厂可以在“真正需要提前暴露时”,决定返回:

  • 原始对象
  • 或代理后的早期对象

所以三级缓存不是为了凑数,而是为了兼容 AOP。

二级缓存只能提前放对象,但 Spring 在循环依赖早期还不一定能马上决定该放原始对象还是代理对象。三级缓存多放了一层工厂,相当于先不急着定,等真的有人来拿时,再决定给原始对象还是代理对象。这一层主要就是为了兼容 AOP。

解决循环依赖过程

  • A 依赖 B
  • B 依赖 A

并且都是单例,字段注入。

第一步:创建 A

  • 实例化 A
  • 此时 A 还没注入 B
  • 把 A 的早期引用工厂放入三级缓存

第二步:A 发现依赖 B

  • 去创建 B

第三步:创建 B

  • 实例化 B
  • B 发现依赖 A

第四步:B 去找 A

  • 一级缓存没有成品 A
  • 二级缓存可能还没有
  • 就从三级缓存里拿 A 的工厂
  • 通过工厂生成 A 的早期引用
  • 放入二级缓存
  • 返回给 B

第五步:B 创建完成

  • B 成为成品,放入一级缓存

第六步:回到 A

  • 把 B 注入给 A
  • A 也创建完成,放入一级缓存

为什么说 Spring 主要解决的是单例循环依赖

因为三级缓存这套机制,本来就是围绕单例 Bean 的缓存和复用设计的。

而 prototype Bean 每次都要新建,不会像单例那样被缓存管理。

所以 prototype 的循环依赖,Spring 一般没法用这套机制救。

注解

@Autowired@Resource

@Autowired 是 Spring 的注解,默认按类型注入。
@Resource 是 JDK/JSR 标准注解,默认按名称注入。

写法

@Autowired`

1
2
3
4
5
6
@Service
public class UserController {

@Autowired
private UserService userService;
}

@Resource`

1
2
3
4
5
6
@Service
public class UserController {

@Resource
private UserService userService;
}

@Autowired 是怎么注入的

@Autowired 默认:先按类型找 Bean。

比如字段类型是:

1
private UserService userService;

Spring 就先去容器里找 UserService 类型的 Bean。

如果只找到一个那就直接注入。

如果找到多个同类型 Bean

就会有歧义,这时通常要结合:

  • @Qualifier
  • @Primary

来进一步指定。

@Resource 是怎么注入的

@Resource 默认:先按名称找,再按类型找。

比如:

1
2
@Resource
private UserService userService;

Spring 会先拿字段名:

1
userService

当作 Bean 名字去找。

如果找到同名 Bean直接注入。

如果没找到同名 Bean再尝试按类型找。

对比

对比点 @Autowired @Resource
来源 Spring JSR / Jakarta 标准
默认注入方式 按类型 先按名称,再按类型
多实现类场景 常配合 @Qualifier / @Primary 可用 name 指定
是否支持 required=false 支持 不常这样用

@Bean@Component

@Component 是把“类”交给 Spring 管理。
@Bean 是把“方法返回的对象”交给 Spring 管理。

写法

@Component`

1
2
3
@Component
public class UserService {
}

Spring 扫描到这个类后,会把这个类实例化成 Bean。

也就是说,Bean 来源是这个类本身。

@Bean`

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public UserService userService() {
return new UserService();
}
}

Spring 会把 userService() 方法返回的对象注册成 Bean。

也就是说,Bean 来源是这个方法的返回值。

区别

对比维度 @Component @Bean
本质 类本身交给 Spring 管理 方法返回的对象交给 Spring 管理
注解位置 标在类上 标在方法上
注册方式 通过 组件扫描 注册 Bean 通过 配置类方法执行结果 注册 Bean
依赖的核心机制 @ComponentScan 扫描到类后注册 Spring 解析 @Configuration,执行 @Bean 方法后注册
Bean 来源 当前这个类的实例 @Bean 方法返回的实例
适合场景 自己写的业务类 第三方类、外部类、需要手动控制创建过程的对象
是否适合第三方类 不适合,因为第三方类源码通常不能加注解 很适合,可以手动 new 第三方对象并注册
创建过程控制能力 较弱,通常由 Spring 按默认规则实例化 很强,创建逻辑完全由你在方法里控制
能否自定义构造细节 一般依赖构造器、注入规则 可以自己写任意创建逻辑、赋值逻辑、工厂逻辑
是否需要组件扫描 需要 不依赖目标类被扫描,但承载它的配置类通常要被 Spring 管理
常见搭配 @Service@Repository@Controller @Configuration
使用粒度 面向“类” 面向“对象”
开发体验 简单直接,适合大多数业务开发 灵活强大,适合复杂配置
可读性 一眼看出这个类是 Spring 组件 一眼看出这个对象是通过配置注册的
对象是否必须是当前类 是,通常就是当前类自己成为 Bean 不必须,返回什么对象就注册什么对象
是否能注册多个不同对象 一个类通常对应一个组件定义 一个配置类里可以写多个 @Bean 方法注册多个对象
对业务代码侵入性 需要在类上加注解 不需要改目标类源码,只需要在配置类里写方法
与 Spring Boot 自动配置关系 常用于应用业务层组件 自动配置类内部大量使用 @Bean 注册默认组件
常见例子 UserServiceOrderControllerUserRepository DataSourceObjectMapperRestTemplate
面试关键词 “类级别注解”“组件扫描”“业务类” “方法级别注解”“手动注册”“第三方类”

@Qualifier和@Primary

假设有一个接口:

1
2
3
public interface UserService {
void test();
}

两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service("userServiceA")
public class UserServiceA implements UserService {
@Override
public void test() {
System.out.println("A");
}
}
@Service("userServiceB")
public class UserServiceB implements UserService {
@Override
public void test() {
System.out.println("B");
}
}

然后这样注入:

1
2
@Autowired
private UserService userService;

Spring 会按类型找 UserService,结果发现有两个:

  • userServiceA
  • userServiceB

它就不知道该注入谁了。

@Qualifier 是怎么解决的

@Qualifier 的作用是:

在多个同类型 Bean 中,明确指定要哪一个。

例如:

1
2
3
@Autowired
@Qualifier("userServiceA")
private UserService userService;

这就表示:

  • 先按类型找 UserService
  • 发现有多个候选
  • 再按 @Qualifier("userServiceA") 指定名字选中 userServiceA

所以 @Qualifier 本质上是:

精确点名。


@Primary 是怎么解决的

@Primary 的作用是:

在多个同类型 Bean 中,指定一个“默认优先候选”。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
@Primary
public class UserServiceA implements UserService {
@Override
public void test() {
System.out.println("A");
}
}
@Service
public class UserServiceB implements UserService {
@Override
public void test() {
System.out.println("B");
}
}

这时候你再写:

1
2
@Autowired
private UserService userService;

Spring 会默认注入 UserServiceA,因为它被标了 @Primary

所以 @Primary 本质上是:

默认优先。

对比

对比点 @Primary @Qualifier
作用位置 Bean 定义处 注入点
作用方式 指定默认优先 Bean 明确指定某个 Bean
适合场景 有一个主实现 不同地方要不同实现
优先级 默认规则 高于 @Primary

@Configuration

**@Configuration 本质上是“配置类”标记。

它和普通类最大的区别在于,Spring 会对 @Configuration 标注的类进行 CGLIB 代理增强,从而拦截类中 @Bean 方法的调用,保证这些方法返回的是 Spring 容器中的单例 Bean,而不是每次方法调用都创建一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class AppConfig {

@Bean
public A a() {
return new A();
}

@Bean
public B b() {
return new B(a());
}
}

如果是 @Configuration,这里 b() 里的 a() 拿到的通常不是全新 A,
而是 Spring 容器中的那个 A Bean。

如果不是标准 @Configuration 增强语义,就可能变成:

1
new A()

又造一个。

@Autowired 底层是怎么注入的

它发生在 Bean 创建流程的哪一步

你前面学过,Bean 创建大致是:

  1. 实例化
  2. 依赖注入
  3. 初始化
  4. 后置处理
  5. 放入容器

@Autowired 主要发生在:

实例化之后,初始化之前的“属性填充/依赖注入阶段”

也就是:

  • Bean 先被创建出来
  • Spring 再检查它有哪些依赖要注入
  • 然后把依赖塞进去

所以:

@Autowired 注入发生在 Bean 已经出生,但还没初始化完成的时候。

谁负责扫描注入点

AutowiredAnnotationBeanPostProcessor

它专门负责扫描并处理 Bean 中的 @Autowired@Value@Inject 注解。

它会做两件大事:

  1. 找出这个 Bean 上哪些位置需要注入
  2. 在合适时机完成注入

@Autowired 底层核心处理器是 AutowiredAnnotationBeanPostProcessor

如果找不到要注入的Bean:

默认情况下,Spring 会报错。

因为 @Autowired 默认是:

1
required = true

如果写:

1
2
@Autowired(required = false)
private UserService userService;

那找不到时,Spring 可以不报错,直接不注入

字段注入底层

1
private UserService userService;

一旦确定了要注入的具体 Bean 实例,Spring 会通过 JDK 的 反射机制Field.set()Method.invoke())打破私有权限限制,将对象塞进去。

setter 注入底层

1
2
3
4
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}

Spring 找到 userService 这个依赖后,本质是通过反射调用 setter:

1
method.invoke(bean, dependencyBean);

所以:

  • 字段注入:反射设字段
  • setter 注入:反射调方法

构造器注入底层

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {

private final UserService userService;

public OrderService(UserService userService) {
this.userService = userService;
}
}

Spring 在实例化 OrderService 的时候,就会先分析构造器参数:

  • 需要一个 UserService
  • 先去容器里找 UserService
  • 再把它作为参数传给构造器
  • 然后再创建对象

所以构造器注入和字段注入最大的区别是:

字段注入发生在实例化之后;构造器注入发生在实例化时。