Spring
Spring是什么
Spring 是一个轻量级 Java 开发框架,核心思想是 IoC 和 AOP。
IoC 用来管理对象的创建和依赖关系,降低耦合;AOP 用来把事务、日志、权限这些公共逻辑从业务代码中抽离出来。
在此基础上,Spring 又提供了 MVC、事务管理、整合数据库等能力,所以它成了 Java 后端开发的核心框架。
没有 Spring:
1 | UserService userService = new UserService(); |
有 Spring:
1 | @Service |
区别:
- 以前你自己创建对象
- 现在 Spring 帮你创建对象
- 以前你自己维护依赖关系
- 现在 Spring 帮你注入依赖
IoC 和 DI
IoC:控制反转
DI:依赖注入
IoC 是控制反转思想,DI 是 IoC 的实现方式之一。
通过 IoC 和 DI,Spring 可以降低对象之间的耦合,提高系统的可维护性和扩展性。
什么叫“控制反转”
先看不用 Spring 的情况。
假设有这两个类:
1 | public class UserService { |
这里谁在“控制”对象的创建?
答案是:UserController 自己控制 UserService 的创建。
也就是说:
- 需要谁,就自己
new - 对象什么时候创建、怎么创建,都由程序员自己决定
控制权在对象自己手里
如果用了 Spring:
1 | @RestController |
这时候你没有自己 new UserService()。
而是 Spring 容器在启动时帮你创建好 UserService,然后再放进 UserController 里。
也就是说:
- 以前:对象自己创建依赖对象
- 现在:对象不再自己创建,交给 Spring 容器来创建和管理
这就叫 控制反转(IoC)。
一句话理解
原来对象的控制权在程序员手里,现在反转给 Spring 容器了。
DI 是什么
DI 是 Dependency Injection,依赖注入。
意思是:
一个对象依赖另一个对象时,不自己创建,而是由容器把依赖对象注入进来。
比如:
1 | UserController` 依赖 `UserService |
Spring 把 UserService 塞给 UserController
这就叫 依赖注入
DI 常见注入方式
依赖注入有哪些方式?
1)构造器注入
1 |
|
2)Setter 注入
1 | @RestController |
3)字段注入
1 | @RestController |
其中构造器注入最推荐,因为依赖关系清晰,适合不可变对象,也方便单元测试。
字段注入虽然写法简洁,但不利于测试和维护。
实现底层
| 注入方式 | 执行时机 | 底层手段 | 是否推荐 |
|---|---|---|---|
| 构造器注入 | Bean 实例化阶段,调用构造方法 | 反射执行构造器 | ✅ Spring 首选推荐 |
| Setter 注入 | populateBean 属性填充阶段,对象创建之后 | 反射调用 set 方法 | 可选依赖使用 |
| 字段注入 | populateBean 属性填充阶段,对象创建之后 | 暴力反射 Field.set | ❌ 不推荐,仅代码简洁 |
Bean
Bean 就是交给 Spring 容器管理的对象。
Bean 和普通对象的区别
Bean 本质上也是对象。
只是它比普通对象多了一层身份:
- 普通对象:你自己 new 出来的
- Bean:Spring 创建、保存、管理的对象
比如这个类:
1 | @Service |
Spring 扫描到 @Service 后,会把 UserService 创建出来放进容器里。
这个 UserService 对象就是 Bean。
创建Bean
方式一:用注解声明
最常见。
1 | @Service |
或者:
1 | @Component |
Spring 扫描到这些注解后,就会把类实例化成 Bean。
常见注解有:
@Component@Service@Repository@Controller@RestController
这些本质上都和 Bean 有关。
方式二:用 @Bean 方法注册
1 | @Configuration |
这里 userService() 返回的对象,也会被放进 Spring 容器。
这也是 Bean。
这个方式常用于:
- 第三方类不能加注解
- 手动控制创建逻辑
方式三:XML 配置
1 | <bean id="userService" class="com.example.UserService"/> |
Bean 在容器里长什么样
可以把 Spring 容器想象成一个大仓库。
仓库里存很多对象:
userServiceorderServiceuserControllerdataSource
这些对象都带着自己的信息,比如:
- 名字
- 类型
- 作用域
- 是否单例
- 初始化方法
- 销毁方法
- 依赖关系
Spring 就根据这些信息来管理 Bean。
所以 Bean 不只是“一个对象”,它还有一套元信息。
Bean = 对象 + 被 Spring 管理的配置信息
Bean 有什么好处
把对象变成 Bean 后,Spring 就能帮我们做很多事。
1)统一创建
你不用自己 new
2)统一注入依赖
配合 @Autowired
3)统一管理生命周期
比如初始化、销毁
4)统一扩展功能
比如事务、AOP、代理
所以 Bean 是 Spring 一切能力的基础。
很多能力不是直接加在“类”上,而是加在“Bean”上。
Bean的生命周期
实例化 → 依赖注入 → 初始化 → 使用 → 销毁
在 Spring 看来,Bean 在真正创建前,先得有一份“说明书”。
这份说明书里会写:
- 这个 Bean 的名字是什么
- 它的类型是什么
- 它是单例还是多例
- 它依赖谁
- 它有没有初始化方法
- 它有没有销毁方法
Spring 把这份“说明书”叫:
BeanDefinition
所以 Bean 创建的前提是:
Spring 先拿到 BeanDefinition,再根据定义去创建 Bean。
第一步:实例化 Bean
Spring 先根据 Bean 定义,把对象创建出来。
比如:
1 | @Service |
Spring 容器启动时,会先想办法把 UserService 对象创建出来。
第二步:依赖注入
比如:
1 | @Service |
Spring 创建完 OrderService 后,会继续把它依赖的 UserService 注入进去。
这一步后,对象和它需要的依赖才真正连起来。
第三步:初始化
对象创建了,依赖也注入了,但有时候还需要做一些“准备动作”。
比如:
- 建立连接
- 读取配置
- 初始化缓存
- 做一些校验
Spring 就提供了初始化阶段。
常见方式有:
方式 1:@PostConstruct
1 | @Service |
方式 2:实现 InitializingBean
1 | @Service |
方式 3:指定 init-method
1 | <bean id="userService" class="com.example.UserService" init-method="init"/> |
第四步:使用
初始化完成后,这个 Bean 就进入可用状态了。
比如:
- 被 Controller 调用
- 被 Service 使用
- 被事务代理增强
- 被 AOP 拦截
这时候它就开始正常参与业务运行。
这个阶段其实没什么复杂的,就是:
Bean 已经准备好,可以被容器和业务代码使用了。
第五步:销毁
当容器关闭时,Spring 会把 Bean 销毁。
比如项目停掉时,某些资源需要释放:
- 关闭数据库连接
- 关闭线程池
- 清理缓存
- 释放文件句柄
Spring 会在销毁前调用一些回调方法。
常见方式有:
方式 1:@PreDestroy
1 | @Service |
方式 2:实现 DisposableBean
1 | @Service |
方式 3:指定 destroy-method
1 | <bean id="userService" class="com.example.UserService" destroy-method="destroy"/> |
什么是 Aware 回调
这是中间常见的一步。
如果一个 Bean 实现了某些 Aware 接口,Spring 会把一些容器信息“告诉它”。
比如:
BeanNameAwareBeanFactoryAwareApplicationContextAware
例子:
1 | @Service |
意思是 Spring 会在生命周期过程中,把当前 Bean 的名字传给它。
什么是 BeanPostProcessor
这个也很高频。
它的作用是:
允许 Spring 在 Bean 初始化前后,对 Bean 做额外处理。
比如 AOP 代理对象,很多时候就是在这个阶段织入的。
简单理解:
- 初始化前,可以加工一下
- 初始化后,也可以加工一下
BeanFactory 和 ApplicationContext 有什么区别?
BeanFactory 是 IoC 容器的基础接口,ApplicationContext 在它的基础上提供了更完整的应用上下文能力。
BeanFactory 主要解决 Bean 的创建、获取和依赖管理。ApplicationContext 除了这些,还提供事件发布、国际化、资源访问、环境配置等功能,并在刷新上下文时自动发现、注册和执行相关后置处理器。
创建时机上也经常被问到:普通 BeanFactory 的 Bean 通常按需创建;ApplicationContext 刷新时,通常会提前实例化非懒加载的单例 Bean,让依赖或配置错误尽早暴露。这不代表 ApplicationContext 会启动时创建所有 Bean,懒加载单例和 prototype 仍然有不同的创建时机。
实际业务开发里我们通常使用 ApplicationContext,Spring Boot 启动时也会创建对应类型的 ApplicationContext。
单例 Bean 和 prototype Bean
singleton 是整个 Spring 容器里通常只有一个 Bean 实例。
1 | @Service |
1 | UserService a = context.getBean(UserService.class); |
prototype 是每次获取 Bean 时,都会创建一个新的实例。
1 | @Service |
1 | UserService a = context.getBean(UserService.class); |
Spring 默认是 singleton
因为大多数业务类其实不需要创建很多份。
singleton 适合什么场景
无状态 Bean
所谓无状态,就是类里不保存“每次请求独有的数据”。
prototype 适合什么场景
适合:
一个 Bean 需要保存独立状态
singleton Bean 生命周期
Spring 会比较完整地管理它:
- 创建
- 依赖注入
- 初始化
- 使用
- 销毁
当容器关闭时,Spring 会调用它的销毁方法。
prototype Bean 生命周期
Spring 通常只负责前半段:
- 创建
- 依赖注入
- 初始化
但不负责完整销毁。
也就是说:
拿到 prototype Bean 后,后面它什么时候不用了、怎么释放资源,Spring 通常不管。
创建时机的区别
singleton
一般在容器启动时,默认会提前创建单例 Bean。
所以项目一启动,它们很多就已经准备好了。
加 @Lazy,可以第一次 getBean 才创建。
prototype
不会在容器启动时统一创建。
通常是你每次调用 getBean() 或每次真正需要它时,才创建新的对象。
单例 Bean 线程安全问题
Spring 单例是容器层面单例,对象本身是否线程安全看代码。如果 Bean 里有可变成员变量,多线程并发会有线程安全问题;无状态 Bean(没有可变成员)安全。
对比
| 对比点 | singleton | prototype |
|---|---|---|
| 实例数量 | 容器中通常一个 | 每次获取都新建 |
| 是否默认 | 是 | 否 |
| 获取 Bean 时 | 多次拿到同一个对象 | 多次拿到不同对象 |
| 创建时机 | 常见是容器启动时创建 | 获取时创建 |
| 销毁管理 | Spring 会管理 | Spring 通常不负责销毁 |
| 适合场景 | 无状态共享对象 | 有状态独立对象 |
| 线程安全 | 要特别注意 | 相对更少共享问题 |
AOP
面向切面编程
它本质上就是:
把那些和业务无关、但很多地方都要用的公共逻辑,抽出来统一处理。
比如:
- 日志
- 事务
- 权限校验
- 性能统计
- 异常处理
这些东西不是某个业务独有的,而是“横着切”很多业务方法。
AOP 解决了什么问题
1)把公共逻辑和业务逻辑分离
业务代码只管业务。
2)减少重复代码
日志、事务、鉴权不用每个方法都手写。
所以 AOP 的价值就是:
解耦 + 复用
AOP的几个概念
切面(Aspect)
切面就是:
封装公共逻辑的类
比如日志切面、事务切面、权限切面。
1 | @Aspect |
这个类就是一个切面。
连接点(JoinPoint)
连接点就是:
程序运行过程中,可以被拦截的位置
在 Spring AOP 里,最常见的连接点其实就是:
方法执行
哪些方法可以被增强,那些方法执行点就是连接点。
切点(Pointcut)
切点就是:
到底要拦截哪些连接点
也就是从所有方法里,挑出要增强的那一部分。
比如:
1 | execution(* com.example.service..*(..)) |
意思是:
拦截 service 包下所有类的所有方法。
这就是切点表达式。
通知(Advice)
通知就是:
在目标方法的什么时机,执行什么增强逻辑
常见通知有:
@Before:方法执行前@After:方法执行后@AfterReturning:方法正常返回后@AfterThrowing:方法抛异常后@Around:环绕通知,功能最强
比如:
1 | @Before("execution(* com.example.service..*(..))") |
织入(waving)
把切面应用到目标对象,从而创建代理对象的过程。
目标对象(Target)
目标对象就是:
原本真正执行业务逻辑的那个对象
代理对象(Proxy)
Spring AOP 真正运行时,不一定直接用目标对象,而是会创建一个代理对象。
AOP 框架创建的对象,它包含了目标对象的所有方法,并织入了切面逻辑。
AOP 是怎么做到“无侵入增强”的?
动态代理
Spring AOP 不是直接改你的源码,
而是给你的 Bean 包一层代理对象。
调用流程大概变成:
调用者 → 代理对象 → 目标对象
代理对象负责插入日志、事务、权限这些逻辑。
1 | orderService.createOrder(); |
如果这个方法被 AOP 增强了,实际可能不是直接调 OrderService,而是:
- 先进入代理对象
- 执行前置通知
- 调用目标方法
createOrder() - 执行后置通知
- 返回结果
OOP:面向对象编程
JDK 和 CGLIB 动态代理
JDK 动态代理是什么
JDK 动态代理是 Java 自带的代理机制。
它的特点是:
它要求目标对象必须实现接口。
比如:
1 | public interface UserService { |
这时候 JDK 就可以根据接口 UserService,动态生成一个代理对象。
这个代理对象:
- 看起来也是
UserService - 实际内部会拦截方法调用
- 再把调用转发给目标对象
CGLIB 动态代理是什么
CGLIB 是一种基于字节码生成的代理方式。
它的特点是:
不要求目标类实现接口。
它是怎么做的:
直接继承目标类,然后重写目标方法,在重写的方法里加增强逻辑。
比如:
1 | public class UserService { |
CGLIB 会生成一个类似这样的子类:
1 | public class UserServiceProxy extends UserService { |
当然这不是它真实源码,只是帮助理解。
区别
| 特性 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 实现原理 | 基于接口(反射机制) | 基于继承(底层字节码技术) |
| 代理对象关系 | 代理类与目标类是兄弟关系 | 代理类是目标类的子类 |
| 限制条件 | 目标类必须实现至少一个接口 | 目标类/方法不能被 final 修饰 |
| 底层库 | Java 内置(java.lang.reflect.Proxy) |
第三方库(net.sf.cglib.proxy) |
| 执行效率 | 在新版 JDK 中效率非常高 | 代理创建慢,但执行方法时效率略高 |
Spring 更喜欢先用 JDK 动态代理
因为 JDK 动态代理是 Java 原生支持的,比较标准,也不需要额外通过继承去生成子类
循环依赖
循环依赖就是两个或多个对象在创建过程中,互相依赖对方。
1 | @Component |
1 | @Component |
有点像死锁
Spring 一定解决不了循环依赖吗
不是。
Spring 能解决一部分循环依赖,但不是所有循环依赖都能解决。
最常见的说法是:
Spring 默认主要能解决单例 Bean 的 setter/字段注入循环依赖。
但:
- 构造器循环依赖,通常解决不了
- prototype 循环依赖,通常也解决不了
字段/Setter 注入:先实例化,再注入,Spring 有机会提前暴露对象。
构造器注入:创建时就必须拿到完整依赖,所以循环依赖通常无解。
Spring 解决循环依赖靠什么
三级缓存
一级缓存:成品 Bean
二级缓存:提前暴露的半成品 Bean
三级缓存:生成早期 Bean 引用的工厂(ObjectFactory)
为什么不能只有两级缓存
二级缓存只能提前放对象,但 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 一般没法用这套机制救。
Spring 事务
事务就是一组操作作为一个整体执行,要么全部成功,要么全部失败回滚。
而Spring 事务就是把事务控制从业务代码里抽出来,交给 Spring 统一管理。
用法
1 | @Service |
虽然没手动写:
- begin
- commit
- rollback
但 Spring 会帮你处理。
原理
AOP + 代理对象
调用:
1 | accountService.transfer(); |
实际可能不是直接进 transfer(),而是:
- 先进入代理对象
- 代理对象先开启事务
- 再调用目标对象的
transfer() - 如果成功,提交事务
- 如果抛异常,回滚事务
可以脑补成:
1 |
|
声明式事务
Spring 事务管理有两种思路:
1)编程式事务
自己写事务控制代码。
比如手动写:
1 | try { |
2)声明式事务
只声明“这个方法需要事务”,具体怎么开、怎么提交、怎么回滚,交给 Spring。
最典型就是:
1 | @Transactional |
现在项目里最常说的 Spring 事务,通常指的就是:
声明式事务
Spring 事务默认什么时候回滚
默认情况下:
Spring 只对运行时异常 RuntimeException 和 Error 回滚。
比如:
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 | @Service |
- 原因:AOP 是通过代理对象(Proxy)拦截调用的。在类内部通过
this.B()调用时,是原始对象在执行,绕过了代理对象,事务逻辑(切面)也就没机会执行。 - 对策:将 B 移到另一个 Service,或者通过
AopContext.currentProxy()获取当前代理对象再调用。
方法非 public 修饰: 如果方法是 private、protected 或 default,事务通常会失效。
- 原因:Spring 事务拦截器默认只检查
public方法。此外,JDK 代理要求方法必须在接口中定义,而 CGLIB 代理通过继承实现,无法重写private方法。
还有可能是抛出的异常不对:spring事务只对 RuntimeException 和 Error 回滚。
也可能Bean 没被 Spring 管理
只有 Spring 容器中的 Bean,Spring 才能给它生成事务代理。
Spring MVC
负责把浏览器请求,交给后端 Java 方法处理,再把结果返回给前端的一套机制。
DispatcherServlet
-> HandlerAdapter
-> 调用 Controller 方法
-> 方法内部业务逻辑执行
-> 拿到返回值
Spring MVC 执行流程
第一步:浏览器发送请求
比如浏览器访问:
1 | GET /user/get?id=1 |
或者前端发送一个 POST 请求:
1 | POST /user/save |
这个 HTTP 请求会先被 Web 容器接收,比如 Tomcat。
然后再交给 Spring MVC 的 DispatcherServlet。
第二步:DispatcherServlet 接收请求
DispatcherServlet 收到请求后,不会自己处理业务。
它的职责更像“总控台”,不是“干业务的人”。
DispatcherServlet默认web 容器启动时初始化
第三步:通过 HandlerMapping 找到处理器
Spring MVC 要先知道:
这个请求该由哪个 Controller 的哪个方法处理。
这件事通常由HandlerMapping来完成。
比如有 Controller:
1 | @RestController |
当请求是:
1 | /user/get?id=1 |
HandlerMapping 会把它匹配到:
1 | UserController#getUser(Integer id) |
HandlerMapping 的作用是根据请求路径,找到要执行的目标方法。
第四步:通过 HandlerAdapter 调用目标方法
找到方法后,还不能直接粗暴调,因为 Spring MVC 还要处理很多细节:
- 参数绑定
- 注解解析
- 请求体转换
- 返回值处理准备
所以 Spring MVC 不会自己直接硬调 Controller,而是通过:
HandlerAdapter来适配调用。
可以把它理解成:
真正负责“把请求转换成方法调用”的执行器。
参数绑定
比如你的 Controller 方法是:
1 | @GetMapping("/get") |
请求是:
1 | /user/get?id=1 |
Spring MVC 会自动把请求参数里的:
1 | id=1 |
绑定到方法参数:
1 | Integer id |
再比如:
1 | @GetMapping("/user/{id}") |
请求:
1 | /user/1 |
Spring MVC 会把路径中的 1 绑定给 id。
还有 POST JSON 请求:
1 | @PostMapping("/save") |
Spring MVC 会把请求体 JSON 转成 Java 对象。
所以这一阶段,本质上是:
把 HTTP 请求的数据,转换成 Controller 方法需要的参数。
调用 Controller 方法
参数准备好后,就真正执行 Controller 方法。
比如:
1 | public String getUser(Integer id) { |
这一步才真正开始执行业务逻辑。
当然很多时候 Controller 还会再调用 Service:
1 | public UserVO getUser(Integer 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 | @GetMapping("/get") |
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 | public class MyInterceptor implements HandlerInterceptor { |
preHandle:进 Controller 前postHandle:Controller 后,视图渲染前afterCompletion:整个请求结束后
preHandle拦截器用 .addPathPatterns(“/**”)判断过滤路径;
用 .order() 指定顺序,数值越小,preHandle() 越先执行。
AOP
属于 Spring 框架
作用在 方法层面
1 | 请求 |
如果 Controller 里再调 Service,而 Service 上有 AOP:
1 | Controller |
对比
| 对比点 | Filter | Interceptor | AOP |
|---|---|---|---|
| 所属 | Servlet 规范 | Spring MVC | Spring |
| 作用位置 | DispatcherServlet 之前 | Controller 前后 | 方法调用前后 |
| 拦截对象 | HTTP 请求 | Controller 请求 | Bean 方法 |
| 依赖 Spring 吗 | 不依赖 | 依赖 | 依赖 |
| 典型场景 | 编码、跨域、底层日志 | 登录校验、接口日志 | 事务、方法日志、权限、监控 |
控制器注解@Controller @RestController
@Controller
标识这是控制器,放到 Spring 容器。返回 String 默认是视图名,不会直接返回 JSON。
@RestController
@Controller + @ResponseBody 组合注解。 类上加上之后,当前类所有方法返回值都直接写入响应体,不走视图解析,前后端分离接口用。
请求映射注解@RequestMapping
建立请求 URL 和 Controller 方法的映射。 属性:value/path路径、method请求方式。
衍生简写注解(底层都是 @RequestMapping)
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
接收参数注解@RequestParam、@PathVariable、@RequestBody
@RequestParam 是什么
它用来接收 URL 里的 查询参数,或者表单参数。
比如请求:
1 | /user/get?id=1&name=Tom |
Controller:
1 | @GetMapping("/user/get") |
这里:
id=1name=Tom
就是请求参数。
@PathVariable 是什么
它用来接收 URL 路径中的动态部分,更符合 RESTful 风格
比如请求:
1 | /user/1 |
Controller:
1 | @GetMapping("/user/{id}") |
@RequestBody 是什么
它用来接收 请求体中的数据,通常是 JSON。
比如前端发 POST 请求:
1 | { |
Controller:
1 | @PostMapping("/user/save") |
Spring MVC 会把请求体中的 JSON 自动转成 User 对象。
前后端分离接口里非常常见,提交 JSON 数据时,基本就会用 @RequestBody。
对比
| 注解 | 数据来源 | 常见场景 |
|---|---|---|
@RequestParam |
查询参数 / 表单参数 | ?id=1 |
@PathVariable |
URL 路径变量 | /user/1 |
@RequestBody |
请求体 body | JSON 提交 |
Spring Boot
Spring Boot比Spring好在哪
1.自动化配置,根据依赖自动配好常用的组件
2.推出了 Starter 依赖机制,能快速启动
3.内嵌 Web 容器(Tomcat/Jetty/Undertow),不需要外部部署 Tomcat
Spring Boot自动配置
Spring Boot 会根据当前项目里的依赖、环境、配置文件等条件,自动决定要不要创建某些 Bean。
比如:
- 引入了 web 依赖
→ 自动配置 MVC 相关组件 - 引入了数据源依赖并写了数据库配置
→ 自动配置 DataSource - 引入了 Jackson
→ 自动配置 JSON 转换器
所以自动配置的核心逻辑是:
按条件装配 Bean。
自动装配 (Autowiring)
所属领域: Spring Framework (IoC 容器)
自动装配是解决 Bean 与 Bean 之间依赖注入(DI) 的问题。当一个组件需要另一个组件作为成员变量时,Spring 自动帮你把这个依赖找出来并注入进去。
- 核心注解:
@Autowired、@Resource、@Inject。 - 工作机制: 扫描 Spring 容器中已经存在的 Bean,根据类型(byType)或名称(byName)进行关联。
比如
pom.xml 加 starter
→ Maven 下载依赖
→ 类进入 classpath
→ @EnableAutoConfiguration 加载自动配置类
→ 条件匹配成功
→ 自动注册 Bean
→ 功能可用
@SpringBootApplication
它本质上包含了三个关键注解:
@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan
其中和自动配置最相关的,是:
@EnableAutoConfiguration
@EnableAutoConfiguration
它会触发 Spring Boot 去加载一批“自动配置类”。
这些自动配置类通常长这样:
DispatcherServletAutoConfigurationDataSourceAutoConfiguration
@EnableAutoConfiguration = 告诉 Spring Boot:去把那些自动配置类找出来,看看哪些该生效。
自动配置类从哪来
Spring Boot 会从依赖包里去找自动配置类。
老版本常见是:
1 | META-INF/spring.factories |
新版 Spring Boot 里更常见的是:
1 | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
这些文件里会列出很多自动配置类的全限定名。
@SpringBootConfiguration
本质就是@Configuration:当前类是配置类,可以用 @Bean 注册 Bean
@ComponentScan
扫描当前包及其子包下的组件,并注册到 Spring 容器中。
比如有这些类:
1 | @Service |
只要它们在启动类所在包或其子包下,@ComponentScan 就能扫到它们。
然后这些类就会变成 Bean。
约定大于配置
约定大于配置:框架预先定好一套默认规则(约定),开发者只要遵守这套约定,就不用写大量配置;如果不满足默认约定,才需要手动修改配置。
比如:
组件扫描路径默认扫描主启动类(@SpringBootApplication)所在包及其所有子包下的 @Component、@Service 等注解。
配置文件与位置默认自动加载 resources 目录下的 application.properties 或 application.yml。
为什么自己定义的 Bean 能覆盖自动配置的 Bean
因为 Spring Boot 的自动配置通常都会配合 @ConditionalOnMissingBean
它的意思是:
只有当容器里还没有这个 Bean 时,自动配置才会生效。
例如某个自动配置类里可能有:
1 | @Bean |
这段话的意思就是:
- 如果容器里没有
ObjectMapper - 那我就自动创建一个默认的
ObjectMapper
但如果已经写了:
1 | @Configuration |
那 Spring Boot 就会发现:
“容器里已经有 ObjectMapper 了。”
于是自动配置里的这个 Bean 就不会再创建。
注解
@Autowired和@Resource
@Autowired 是 Spring 的注解,默认按类型注入。
@Resource 是 JDK/JSR 标准注解,默认按名称注入。
写法
@Autowired`
1 | @Service |
@Resource`
1 | @Service |
@Autowired 是怎么注入的
@Autowired 默认:先按类型找 Bean。
比如字段类型是:
1 | private UserService userService; |
Spring 就先去容器里找 UserService 类型的 Bean。
如果只找到一个那就直接注入。
如果找到多个同类型 Bean
就会有歧义,这时通常要结合:
@Qualifier@Primary
来进一步指定。
@Resource 是怎么注入的
@Resource 默认:先按名称找,再按类型找。
比如:
1 | @Resource |
Spring 会先拿字段名:
1 | userService |
当作 Bean 名字去找。
如果找到同名 Bean直接注入。
如果没找到同名 Bean再尝试按类型找。
对比
| 对比点 | @Autowired |
@Resource |
|---|---|---|
| 来源 | Spring | JSR / Jakarta 标准 |
| 默认注入方式 | 按类型 | 先按名称,再按类型 |
| 多实现类场景 | 常配合 @Qualifier / @Primary |
可用 name 指定 |
是否支持 required=false |
支持 | 不常这样用 |
@Bean和@Component
@Component 是把“类”交给 Spring 管理。
@Bean 是把“方法返回的对象”交给 Spring 管理。
写法
@Component`
1 | @Component |
Spring 扫描到这个类后,会把这个类实例化成 Bean。
也就是说,Bean 来源是这个类本身。
@Bean`
1 | @Configuration |
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 注册默认组件 |
| 常见例子 | UserService、OrderController、UserRepository |
DataSource、ObjectMapper、RestTemplate |
| 面试关键词 | “类级别注解”“组件扫描”“业务类” | “方法级别注解”“手动注册”“第三方类” |
@Qualifier和@Primary
假设有一个接口:
1 | public interface UserService { |
两个实现类:
1 | @Service("userServiceA") |
然后这样注入:
1 | @Autowired |
Spring 会按类型找 UserService,结果发现有两个:
userServiceAuserServiceB
它就不知道该注入谁了。
@Qualifier 是怎么解决的
@Qualifier 的作用是:
在多个同类型 Bean 中,明确指定要哪一个。
例如:
1 | @Autowired |
这就表示:
- 先按类型找
UserService - 发现有多个候选
- 再按
@Qualifier("userServiceA")指定名字选中userServiceA
所以 @Qualifier 本质上是:
精确点名。
@Primary 是怎么解决的
@Primary 的作用是:
在多个同类型 Bean 中,指定一个“默认优先候选”。
例如:
1 | @Service |
这时候你再写:
1 | @Autowired |
Spring 会默认注入 UserServiceA,因为它被标了 @Primary。
所以 @Primary 本质上是:
默认优先。
对比
| 对比点 | @Primary |
@Qualifier |
|---|---|---|
| 作用位置 | Bean 定义处 | 注入点 |
| 作用方式 | 指定默认优先 Bean | 明确指定某个 Bean |
| 适合场景 | 有一个主实现 | 不同地方要不同实现 |
| 优先级 | 默认规则 | 高于 @Primary |
@Configuration
**@Configuration 本质上是“配置类”标记。
它和普通类最大的区别在于,Spring 会对 @Configuration 标注的类进行 CGLIB 代理增强,从而拦截类中 @Bean 方法的调用,保证这些方法返回的是 Spring 容器中的单例 Bean,而不是每次方法调用都创建一个新的对象。
1 | @Configuration |
如果是 @Configuration,这里 b() 里的 a() 拿到的通常不是全新 A,
而是 Spring 容器中的那个 A Bean。
如果不是标准 @Configuration 增强语义,就可能变成:
1 | new A() |
又造一个。
@Autowired 底层是怎么注入的
它发生在 Bean 创建流程的哪一步
前面学过,Bean 创建大致是:
- 实例化
- 依赖注入
- 初始化
- 后置处理
- 放入容器
@Autowired 主要发生在:
实例化之后,初始化之前的“属性填充/依赖注入阶段”
也就是:
- Bean 先被创建出来
- Spring 再检查它有哪些依赖要注入
- 然后把依赖塞进去
所以:
@Autowired 注入发生在 Bean 已经出生,但还没初始化完成的时候。
谁负责扫描注入点
AutowiredAnnotationBeanPostProcessor
它专门负责扫描并处理 Bean 中的 @Autowired、@Value 和 @Inject 注解。
它会做两件大事:
- 找出这个 Bean 上哪些位置需要注入
- 在合适时机完成注入
@Autowired 底层核心处理器是 AutowiredAnnotationBeanPostProcessor。
如果找不到要注入的Bean:
默认情况下,Spring 会报错。
因为 @Autowired 默认是:
1 | required = true |
如果写:
1 | @Autowired(required = false) |
那找不到时,Spring 可以不报错,直接不注入