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 | @Service |
2)Setter 注入
1 | @RestController |
3)字段注入
1 | @RestController |
其中构造器注入最推荐,因为依赖关系清晰,适合不可变对象,也方便单元测试。
字段注入虽然写法简洁,但不利于测试和维护。
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”上。
BeanFactory 和 ApplicationContext
BeanFactory 是 Spring 最底层的容器接口。
ApplicationContext 是 BeanFactory 的高级版本。
BeanFactory 是什么
BeanFactory 是 Spring 最核心、最基础的容器接口。
它最主要的职责就是:
- 管理 Bean
- 按需获取 Bean
ApplicationContext 是什么
ApplicationContext 是 BeanFactory 的子接口。
它不仅有 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 | @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 代理对象,很多时候就是在这个阶段织入的。
简单理解:
- 初始化前,可以加工一下
- 初始化后,也可以加工一下
单例 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。
所以项目一启动,它们很多就已经准备好了。
prototype
不会在容器启动时统一创建。
通常是你每次调用 getBean() 或每次真正需要它时,才创建新的对象。
对比
| 对比点 | 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 原生支持的,比较标准,也不需要额外通过继承去生成子类
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 的核心入口
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 | @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:整个请求结束后
AOP
属于 Spring 框架
作用在 方法层面
1 | 请求 |
如果 Controller 里再调 Service,而 Service 上有 AOP:
1 | Controller |
对比
| 对比点 | 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 | @GetMapping("/user/get") |
这里:
id=1name=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 | @GetMapping("/user/{id}") |
这里路径中的:
1 | 1 |
会被绑定到 id。
它最常见的场景
RESTful 风格接口很常见:
/user/1/order/1001/product/88
这些路径里的变量就适合用 @PathVariable。
特点
- 值来自 URL 路径
- 常用于资源定位
- 更符合 RESTful 风格
@RequestBody 是什么
它用来接收 请求体中的数据,通常是 JSON。
比如前端发 POST 请求:
1 | { |
Controller:
1 | @PostMapping("/user/save") |
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 去加载一批“自动配置类”。
这些自动配置类通常长这样:
DispatcherServletAutoConfigurationDataSourceAutoConfigurationJacksonAutoConfigurationWebMvcAutoConfiguration
@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 | @Bean |
意思是:
- 你没自己配,我帮你配一个默认的
- 你自己配了,我就不插手
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 | @Service |
只要它们在启动类所在包或其子包下,@ComponentScan 就能扫到它们。
然后这些类就会变成 Bean。
它和 SpringApplication.run() 是什么关系
@SpringBootApplication`
负责声明:
- 这是启动类
- 要开启自动配置
- 要扫描组件
SpringApplication.run(…)`
负责真正启动 Spring Boot 应用。
比如:
1 | SpringApplication.run(DemoApplication.class, args); |
它会去创建 Spring 容器、加载配置、启动内嵌服务器等。
为什么你自己定义的 Bean 能覆盖自动配置的 Bean
因为 Spring Boot 的自动配置通常都会配合 @ConditionalOnMissingBean
它的意思是:
只有当容器里还没有这个 Bean 时,自动配置才会生效。
例如某个自动配置类里可能有:
1 | @Bean |
这段话的意思就是:
- 如果容器里没有
ObjectMapper - 那我就自动创建一个默认的
ObjectMapper
但如果已经写了:
1 | @Configuration |
那 Spring Boot 就会发现:
“容器里已经有 ObjectMapper 了。”
于是自动配置里的这个 Bean 就不会再创建。
循环依赖
循环依赖就是两个或多个对象在创建过程中,互相依赖对方。
1 | @Component |
1 | @Component |
有点像死锁
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 | @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 可以不报错,直接不注入
字段注入底层
1 | private UserService userService; |
一旦确定了要注入的具体 Bean 实例,Spring 会通过 JDK 的 反射机制(Field.set() 或 Method.invoke())打破私有权限限制,将对象塞进去。
setter 注入底层
1 | @Autowired |
Spring 找到 userService 这个依赖后,本质是通过反射调用 setter:
1 | method.invoke(bean, dependencyBean); |
所以:
- 字段注入:反射设字段
- setter 注入:反射调方法
构造器注入底层
1 | @Service |
Spring 在实例化 OrderService 的时候,就会先分析构造器参数:
- 需要一个
UserService - 先去容器里找
UserService - 再把它作为参数传给构造器
- 然后再创建对象
所以构造器注入和字段注入最大的区别是:
字段注入发生在实例化之后;构造器注入发生在实例化时。
