学习笔记
SpringBoot框架
Spring Bean 基础
一、Spring Bean 基础
1.1 Bean 是单例的吗?
核心要点
- 默认是单例(singleton):Spring IOC 容器中只有一个实例,全局共享
- 多例(prototype):每次请求都创建新实例
- 其他作用域:request(每次HTTP请求)、session(每次HTTP会话)、application(ServletContext生命周期)
详细解析
Spring Bean 的作用域通过 @Scope 注解配置:
| 作用域 | 说明 | 使用场景 |
|---|---|---|
| singleton | 默认值,容器只有一个实例 | 无状态的服务类、配置类 |
| prototype | 每次获取都创建新实例 | 有状态的Bean |
| request | 每个HTTP请求创建一个 | Web应用中请求级别的数据 |
| session | 每个HTTP会话创建一个 | 用户登录信息等会话数据 |
| application | ServletContext生命周期内一个 | 全局应用配置 |
单例 Bean 的特点:
- 容器启动时创建(默认懒加载除外)
- 所有注入该 Bean 的地方引用的是同一个对象
- 节省内存,但需要注意线程安全问题
面试口头回答
Spring Bean 默认是单例的,也就是 singleton 作用域。在 Spring IOC 容器中,一个 Bean 只有一个实例,全局共享,所有注入这个 Bean 的地方引用的都是同一个对象。
除了单例,还有 prototype 多例模式,每次向容器请求的时候都会创建一个新的实例。另外还有 request、session、application 这些 Web 相关的作用域。
单例 Bean 的好处是节省内存,不需要频繁创建销毁对象。但如果 Bean 里有可变的成员变量,多个线程同时访问时可能会有线程安全问题,这一点需要注意。
1.2 Bean 是线程安全的吗?
核心要点
- 单例 Bean 默认不是线程安全的(多个线程共享同一个实例)
- 无状态 Bean 是线程安全的:不存储可变数据,只执行业务逻辑
- 解决方案:使用多例 Bean、设计为无状态、ThreadLocal、加锁
详细解析
为什么单例 Bean 可能线程不安全?
- Spring 默认的 Bean 作用域是 singleton,IOC 容器中只有一个实例,被多个线程共享
- 如果 Bean 维护了可变的成员变量(如计数器、状态标志等),并发访问时可能引发数据不一致
线程安全的情况:
- 无状态 Bean:常见的 Service 或 DAO 层 Bean,不存储可变数据,仅执行业务逻辑,方法内变量都是局部变量,不存在线程安全问题
解决方案:
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| 多例 Bean | @Scope("prototype") | Bean 必须维护状态 |
| 无状态设计 | 避免定义可变成员变量 | 大多数 Service/DAO |
| ThreadLocal | 让每个线程拥有独立变量副本 | 线程上下文数据 |
| 加锁 | synchronized 或 ReentrantLock | 必须共享可变状态 |
最佳实践:
- 尽量将 Bean 设计为无状态
- 需要状态时用 ThreadLocal 或方法参数传递
- 避免在单例 Bean 中使用可变成员变量
面试口头回答
Spring 的单例 Bean 默认不是线程安全的。因为单例 Bean 在容器中只有一个实例,被多个线程共享,如果这个 Bean 维护了可变的成员变量,并发访问时就会引发数据不一致的问题。
但是无状态的 Bean 是线程安全的。比如我们常见的 Service 层或 DAO 层的 Bean,它们通常不存储可变数据,只执行业务逻辑,方法内部都是局部变量,所以不会受并发影响。
如果确实需要保证线程安全,有几种解决方案:
- 使用多例 Bean,每次请求都创建新实例,互不干扰;
- 设计为无状态 Bean,尽量避免定义可变成员变量;
- 使用 ThreadLocal,让每个线程拥有独立的变量副本;
- 加锁,用 synchronized 或 ReentrantLock 保证互斥访问。
实际开发中,我最常用的做法是把 Service 层设计为无状态,需要保存的上下文信息通过方法参数或 ThreadLocal 传递,避免在 Bean 中维护可变状态。
1.3 Bean 的生命周期
核心要点
- 四个主要阶段:实例化 → 属性赋值 → 初始化 → 销毁
- 详细七步:扫描定义 → 实例化 → 属性赋值 → Aware回调 → 前置处理 & 初始化 → 使用 → 销毁
- BeanPostProcessor 在初始化前后提供扩展点,AOP 代理在此生成
详细解析
简化版四阶段:
- 创建 Bean 实例:Spring 根据配置使用反射实例化 Bean
- 属性赋值和依赖注入:解析
@Autowired、@Value等注解,通过构造方法或 setter 注入依赖 - 初始化 Bean:
- 调用 Aware 接口的 set 方法
- 执行 BeanPostProcessor 前置处理
- 调用初始化方法(InitializingBean、@PostConstruct、init-method)
- 执行 BeanPostProcessor 后置处理(可能产生 AOP 代理对象)
- 销毁 Bean:容器关闭时调用 destroy() 方法
详细版七阶段:
扫描与定义阶段(BeanDefinition):Spring 扫描配置文件、注解等,获取 Bean 定义信息,封装成 BeanDefinition。其中记录了类名、作用域、依赖信息以及初始化/销毁方法等元数据。
实例化阶段(Instantiation):Spring 根据 BeanDefinition,通过反射创建 Bean 实例。此时属性还未填充。
属性赋值阶段(Populate Properties / DI):将 Bean 的依赖属性注入,例如通过构造器或 setter 注入。
Aware 接口回调(Aware Callbacks):如果 Bean 实现了
BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口,Spring 会在此阶段回调相应方法,让 Bean 获取容器相关信息。前置处理 & 初始化阶段(BeanPostProcessor + Initializing):
- 前置处理器:调用
BeanPostProcessor.postProcessBeforeInitialization(),可对 Bean 做增强处理 - 初始化:执行
InitializingBean.afterPropertiesSet()、@PostConstruct或 XML 配置的 init-method - 后置处理器:调用
BeanPostProcessor.postProcessAfterInitialization(),对 Bean 做最终增强(例如生成 AOP 代理)
- 前置处理器:调用
使用阶段(Ready to Use):Bean 已经完成初始化,可以被应用程序正常使用。
销毁阶段(Destruction):当容器关闭时,Spring 调用 Bean 的销毁方法,例如
DisposableBean.destroy()、@PreDestroy或 XML 配置的 destroy-method。
面试口头回答
Bean 的生命周期主要包括四个阶段:实例化、属性赋值、初始化和销毁。
更详细来说有七个步骤:
- 第一步是扫描与定义:Spring 扫描配置文件和注解,把 Bean 的定义信息封装成 BeanDefinition;
- 第二步是实例化:通过反射创建 Bean 的对象实例;
- 第三步是属性赋值:解析 Autowired、Value 这些注解,把依赖注入到 Bean 中;
- 第四步是 Aware 回调:如果 Bean 实现了 BeanNameAware、ApplicationContextAware 这些接口,Spring 会回调相应方法,让 Bean 拿到容器的信息;
- 第五步是前置处理和初始化:先执行 BeanPostProcessor 的前置处理,然后执行初始化方法,比如 @PostConstruct 或 afterPropertiesSet,再执行 BeanPostProcessor 的后置处理,这里可能会生成 AOP 代理对象;
- 第六步是使用:Bean 初始化完成,可以被正常使用;
- 第七步是销毁:容器关闭时调用销毁方法,比如 @PreDestroy。
其中 BeanPostProcessor 是很重要的扩展点,Spring 的 AOP 就是在后置处理器中生成代理对象的。
二、Spring 循环依赖
2.1 Spring 中的循环依赖?
核心要点
- 循环依赖:两个或多个 Bean 互相依赖,形成闭环
- Spring 通过三级缓存解决单例 Bean 的循环依赖(构造器注入除外)
- 三级缓存:一级(成品Bean)、二级(半成品Bean)、三级(ObjectFactory)
- 只有单例 Bean 支持循环依赖,prototype 不支持
详细解析
发生场景:
- A 依赖 B,B 依赖 A
- 或者 A → B → C → A 的更复杂循环
三级缓存机制:
| 缓存级别 | 名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 最终形态的 Bean(已实例化、属性填充、初始化) | 单例池,获取 Bean 的主要来源 |
| 二级缓存 | earlySingletonObjects | 过渡 Bean(半成品,尚未属性填充) | 存储三级缓存中 ObjectFactory 产生的对象,防止 AOP 时多次创建代理 |
| 三级缓存 | singletonFactories | ObjectFactory 的 getObject() 方法 | 可以生成原始 Bean 或代理对象,只对单例 Bean 生效 |
解决循环依赖的过程:
- Spring 创建 A 之后,发现 A 依赖了 B,又去创建 B
- B 依赖了 A,又去创建 A
- 在 B 创建 A 的时候,A 发生了循环依赖。由于 A 此时还没有初始化完成,在一二级缓存中肯定没有 A
- 此时去三级缓存中调用
getObject()方法获取 A 的前期暴露对象,也就是调用getEarlyBeanReference()方法,生成一个 A 的早期引用 - 将这个 ObjectFactory 从三级缓存中移除,并将早期暴露对象放入二级缓存
- B 将早期暴露对象注入到依赖中,完成 B 的创建
- B 创建完成后,A 继续完成属性填充和初始化
为什么需要三级缓存?如果只有两级会发生什么?
在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖。但当涉及到 AOP 时,三级缓存就非常重要了:
- 三级缓存中存储的是 ObjectFactory,可以生成代理对象
- 它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象
- 避免了同一个 Bean 有多个代理对象的问题
如果不通过三级缓存,怎么解决循环依赖?
| 方案 | 说明 |
|---|---|
| 重构代码 | 最根本的办法,将相互依赖的功能提取到新类中 |
| Setter 注入替代构造器注入 | 构造器注入在创建 Bean 时就解析依赖,有循环依赖会报错;Setter 注入允许先创建实例再设置依赖 |
| @Lazy 注解 | 延迟加载 Bean,真正需要时才初始化 |
面试口头回答
循环依赖是指两个或多个 Bean 互相依赖持有对方,形成闭环。比如 A 依赖 B,B 又依赖 A。
Spring 主要通过三级缓存来解决单例 Bean 的循环依赖问题。
一级缓存存放最终的成品 Bean,是单例池,我们正常获取 Bean 都是从这里拿。二级缓存存放半成品 Bean,也就是还没完成属性填充和初始化的对象。三级缓存存放的是 ObjectFactory,调用它的 getObject 方法可以生成原始 Bean 或者代理对象。
解决过程是这样的:Spring 创建 A 之后发现 A 依赖 B,就去创建 B;B 又发现依赖 A,再去创建 A。这时候 A 还在创建中,一二级缓存里都没有 A,于是去三级缓存中调用 getObject 获取 A 的早期暴露对象,把这个早期引用注入到 B 中,B 就能完成创建。B 创建完成后,A 继续完成属性填充和初始化。
这里三级缓存比二级缓存多的那一级,主要是为了解决 AOP 代理的问题。如果只有两级缓存,每次获取早期引用都可能创建一个新的代理对象,导致同一个 Bean 有多个代理。三级缓存中的 ObjectFactory 确保多次请求始终返回同一个代理对象。
如果不通过三级缓存解决,也可以用 Setter 注入替代构造器注入,或者用 @Lazy 注解延迟加载,但最根本的解决办法还是重构代码,打破循环依赖。
三、IOC、AOP 与动态代理
3.1 IOC 控制反转
核心要点
- 核心思想:将对象的管理权从应用程序代码转移到 Spring 容器
- DI 依赖注入是 IOC 的具体实现方式
- 传统方式:类 A 依赖类 B,A 自己创建 B 的实例;IOC 方式:Spring 负责实例化和注入 B
- 容器负责 Bean 的实例化、依赖注入、生命周期管理和单例维护
详细解析
控制反转的含义:
- 控制:对象创建、依赖注入、生命周期管理的控制权
- 反转:控制权从开发者手中反转给 Spring 容器
依赖注入的三种方式:
| 注入方式 | 说明 | 示例 |
|---|---|---|
| 构造器注入 | 通过构造方法注入依赖,推荐方式 | @Autowired 放在构造方法上 |
| Setter 注入 | 通过 setter 方法注入依赖 | @Autowired 放在 setter 方法上 |
| 字段注入 | 直接注入到字段上,最简洁但不推荐 | @Autowired 放在字段上 |
注入 Bean 的注解:
@Autowired:默认按照类型注入,是 Spring 提供的注解@Resource:默认按照名称注入,是 JSR-250 标准注解
IOC 流程:
- IOC 容器初始化:解析 XML 配置或注解,获取所有 Bean 的定义信息,生成 BeanDefinition
- Bean 实例化及依赖注入:通过反射实例化 Bean,根据 BeanDefinition 解析依赖关系,通过构造方法、setter 方法完成注入
- Bean 的使用:通过
@Autowired获取当前 Bean
面试口头回答
IOC 控制反转是 Spring 的核心特性。它的核心思想是将对象创建和管理的控制权从开发者手中交给 Spring 容器,实现控制反转。
具体来说,传统方式下,如果类 A 依赖类 B,A 需要自己创建 B 的实例。而在 IOC 模式下,Spring 容器负责实例化 B 并注入到 A 中,A 只需要声明依赖即可。
依赖注入是控制反转的具体实现方式,主要有三种:构造器注入、Setter 注入和字段注入。Spring 推荐用构造器注入,因为依赖明确,而且方便单元测试。
注入 Bean 常用的注解有 @Autowired 和 @Resource。@Autowired 默认按类型注入,是 Spring 提供的;@Resource 默认按名称注入,是 Java 标准注解。
IOC 的流程大致是:容器启动时解析配置和注解,生成 BeanDefinition;然后通过反射实例化 Bean,同时完成依赖注入;最后 Bean 就可以被使用了。
我在项目中主要是配合 Spring Boot 使用 IOC,通过 @Service、@Repository 这些注解声明 Bean,通过 @Autowired 注入依赖,完全不需要手动 new 对象。
3.2 AOP 面向切面编程
核心要点
- 将公共行为和逻辑抽取为可重用模块,减少重复代码
- 底层实现是动态代理,允许在不修改原始代码的情况下扩展方法执行过程
- 核心概念:切面(Aspect)、切点(Pointcut)、通知(Advice)、连接点(JoinPoint)
- Spring 根据目标对象是否实现接口选择代理方式:有接口用 JDK 动态代理,无接口用 CGLIB
详细解析
AOP 核心概念:
| 概念 | 说明 |
|---|---|
| 切面(Aspect) | 横切关注点的模块化,包含通知和切点 |
| 切点(Pointcut) | 定义哪些方法需要被拦截,通过表达式匹配 |
| 通知(Advice) | 拦截后要执行的逻辑,分为 Before、After、Around 等 |
| 连接点(JoinPoint) | 程序执行过程中的某个点,如方法调用 |
| 目标对象(Target) | 被代理的原始对象 |
AOP 执行流程:
- 创建代理对象:Spring 使用 JDK/CGLIB 动态代理,通过
Proxy.newProxyInstance或 Enhancer 生成代理对象 - 拦截切点表达式的方法:Spring 根据目标对象是否实现接口选择代理方式:
- JDK 动态代理:目标对象实现接口 → 生成接口代理
- CGLIB 代理:目标对象没有接口 → 生成子类代理
- 执行增强逻辑:根据通知类型(@Before、@After、@Around),在方法执行前后执行对应的 AOP 逻辑
- 执行目标方法:调用目标对象的方法,完成实际业务逻辑
通知类型:
| 注解 | 执行时机 |
|---|---|
| @Before | 目标方法执行前 |
| @After | 目标方法执行后(无论是否异常) |
| @AfterReturning | 目标方法成功返回后 |
| @AfterThrowing | 目标方法抛出异常后 |
| @Around | 环绕通知,可以控制目标方法是否执行 |
连接点 JoinPoint:
- 用
JoinPoint抽象了连接点,可以获取方法执行时的相关信息 - 如目标类名、方法名、方法参数等
应用场景:
- 日志记录:在方法执行前后自动记录日志
- 事务管理:自动开启、提交或回滚事务(如
@Transactional) - 异常处理:统一捕获和处理异常
- 缓存机制:拦截方法调用,判断是否需要从缓存获取数据
- 统计方法执行时长:性能监控
面试口头回答
AOP 是面向切面编程,它的核心思想是把公共行为和逻辑抽取出来,封装成一个可重用的模块,减少系统中的重复代码。它通过代理机制实现方法增强,允许在不修改原始代码的情况下,对方法的执行过程进行扩展。
AOP 的核心概念有:切面是横切关注点的模块化;切点定义哪些方法需要被拦截;通知是拦截后要执行的逻辑;连接点是程序执行过程中的某个点。
AOP 的执行流程是:首先 Spring 创建代理对象,根据目标对象是否实现接口选择代理方式——有接口用 JDK 动态代理,没接口用 CGLIB 生成子类代理。然后拦截匹配切点表达式的方法,根据通知类型在方法执行前后执行增强逻辑,比如日志记录、事务提交,最后执行目标方法完成实际业务。
通知类型主要有 @Before 前置通知、@After 后置通知、@AfterReturning 返回通知、@AfterThrowing 异常通知、@Around 环绕通知。
我在项目中主要用 AOP 做日志记录和接口耗时统计。定义了一个切面,用 @Around 环绕通知拦截所有 Controller 方法,在方法执行前记录开始时间,执行后计算耗时并打印日志。也用过 @Transactional 做声明式事务管理,这也是 AOP 的典型应用。
3.3 动态代理
核心要点
- 运行时动态生成代理对象,增强目标对象方法
- JDK 动态代理:基于 InvocationHandler 和 Proxy,要求目标类必须实现接口
- CGLIB 动态代理:基于 MethodInterceptor 和 Enhancer,生成目标类的子类,不依赖接口
- Spring AOP 中:有接口默认用 JDK,无接口用 CGLIB
详细解析
动态代理的概念:
- 一种在运行时动态生成代理对象,并在代理对象中增强目标对象方法的技术
- 从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中
- 广泛应用于 AOP、事务管理、日志记录等场景
JDK 动态代理:
核心类:
InvocationHandler接口:重写invoke()方法,在方法调用时执行自定义逻辑Proxy类:通过newProxyInstance()创建代理对象
使用步骤:
- 自定义类实现
InvocationHandler接口,重写invoke方法 - 在
invoke中调用原生方法并自定义处理逻辑 - 通过
Proxy.newProxyInstance()创建代理对象
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法执行前");
Object result = method.invoke(target, args);
System.out.println("方法执行后");
return result;
}
}
CGLIB 动态代理: 核心类:
MethodInterceptor接口:重写intercept()方法拦截增强Enhancer类:通过create()创建代理类
使用步骤:
- 自定义
MethodInterceptor并重写intercept方法 - 通过
Enhancer类的create()创建代理类
JDK vs CGLIB 对比:
| 特性 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 代理方式 | 实现目标类的接口 | 生成目标类的子类 |
| 接口要求 | 必须实现接口 | 不依赖接口 |
| final 限制 | 无 | 不能代理 final 类和方法 |
| 性能 | JDK 版本升级后更优 | 略逊于 JDK(新版本) |
| 底层技术 | 反射 + 接口 | ASM 字节码增强 |
| Spring 默认 | 目标有接口时默认使用 | 目标无接口时使用 |
面试口头回答
动态代理是在运行时动态创建代理对象,对目标对象的方法进行增强。
它的核心原理是基于反射机制:通过反射获取目标类的方法信息,然后在方法执行前后插入自定义逻辑,比如日志、权限验证、事务管理。
Java 中主要有两种动态代理方式:
JDK 动态代理:基于 InvocationHandler 接口和 Proxy 类实现。要求目标类必须实现接口。使用步骤是先实现 InvocationHandler 接口并重写 invoke 方法,在里面调用原生方法并添加自定义逻辑,然后通过 Proxy.newProxyInstance 创建代理对象。代理对象调用方法时,实际会调用 InvocationHandler 的 invoke 方法。
CGLIB 动态代理:通过生成目标类的子类来实现代理,不依赖接口。核心是 MethodInterceptor 接口和 Enhancer 类。它底层使用 ASM 字节码增强技术。
两者的对比:JDK 动态代理只能代理实现了接口的类;CGLIB 可以代理未实现接口的类,但不能代理 final 类型的类和方法。性能方面,随着 JDK 版本升级,JDK 动态代理的性能优势越来越明显。
在 Spring AOP 中,如果 Bean 实现了接口,默认用 JDK 动态代理;如果没有接口,就使用 CGLIB。这个可以通过配置修改。
我在项目中使用动态代理主要是配合 Spring AOP 做日志和事务,没有自己手动实现过动态代理,但理解它的原理。
四、Spring 注解详解
4.1 Spring、SpringMVC、SpringBoot 常用注解
核心要点
- 声明 Bean:@Component、@Controller、@Service、@Repository
- 依赖注入:@Autowired、@Resource
- 作用域:@Scope
- 配置:@Configuration、@Bean
- AOP:@Aspect、@Before、@After、@Around、@Pointcut
- SpringMVC:@RequestMapping、@GetMapping、@PostMapping、@RequestBody、@ResponseBody、@PathVariable
- SpringBoot:@SpringBootApplication、@RestController
详细解析
Spring 注解:
| 注解 | 作用 |
|---|---|
| @Component | 通用标记,声明一个类是 Spring 的 Bean |
| @Controller | 标记控制器层的 Bean |
| @Service | 标记服务层的 Bean |
| @Repository | 标记数据访问层的 Bean(有异常转换功能) |
| @Autowired | 按类型注入 Bean |
| @Resource | 按名称注入 Bean(JSR-250 标准) |
| @Scope | 设置 Bean 作用域(singleton、prototype) |
| @Configuration | 标记为配置类 |
| @Bean | 在配置类中定义 Bean(常用于第三方库) |
| @Aspect | 标记为切面类 |
| @Pointcut | 定义切点表达式 |
SpringMVC 注解:
| 注解 | 作用 |
|---|---|
| @RequestMapping | 映射 HTTP 请求的 URL 路径和方法 |
| @GetMapping | @RequestMapping(method = GET) 的简化 |
| @PostMapping | @RequestMapping(method = POST) 的简化 |
| @RequestBody | 接收 HTTP 请求的 JSON 数据转为 Java 对象 |
| @ResponseBody | 将 Controller 返回的 Java 对象转化为 JSON |
| @PathVariable | 从 URL 路径中获取参数值 |
| @RequestHeader | 从请求头中获取指定信息 |
SpringBoot 注解:
| 注解 | 作用 |
|---|---|
| @SpringBootApplication | 核心组合注解,包含 @Configuration + @EnableAutoConfiguration + @ComponentScan |
| @RestController | @Controller + @ResponseBody 的组合 |
@SpringBootApplication 的组成:
@SpringBootConfiguration:本质上是@Configuration,表明该类是配置类@EnableAutoConfiguration:开启 Spring Boot 的自动配置功能,读取META-INF/spring.factories文件中的类名自动加载 Bean@ComponentScan:启用组件扫描,自动扫描指定包下的@Component、@Service、@Repository、@Controller等注解的类
面试口头回答
Spring 相关的注解我分三类来说:
Spring 核心注解:
- 声明 Bean 的有 @Component 通用标记、@Controller 控制器、@Service 服务层、@Repository 数据层;
- 依赖注入的有 @Autowired 按类型注入、@Resource 按名称注入;
- 配置相关的有 @Configuration 标记配置类、@Bean 在配置类中定义 Bean;
- AOP 相关的有 @Aspect 标记切面类、@Pointcut 定义切点、@Before @After @Around 通知类型。
SpringMVC 注解:
- @RequestMapping 映射 URL 路径和请求方法;
- @GetMapping、@PostMapping 是它的简化形式;
- @RequestBody 把请求的 JSON 转为 Java 对象;
- @ResponseBody 把返回的 Java 对象转为 JSON;
- @PathVariable 从 URL 路径中取参数;
- @RequestHeader 从请求头中取信息。
SpringBoot 注解:
- @SpringBootApplication 是核心组合注解,包含了 @Configuration 表明是配置类、@EnableAutoConfiguration 开启自动配置、@ComponentScan 开启组件扫描;
- @RestController 是 @Controller 加 @ResponseBody 的组合,标记 RESTful API 的控制器。
最常用的就是 @SpringBootApplication 放在启动类上,@RestController 放在 Controller 类上,@GetMapping/@PostMapping 放在方法上映射接口。
五、SpringBoot 自动配置
5.1 SpringBoot 自动配置原理
核心要点
- 基于 @SpringBootApplication 中的 @EnableAutoConfiguration 触发
- 通过 @Import 导入配置选择器,读取 META-INF/spring.factories 文件
- 根据 @Conditional 系列注解过滤配置类
- 最后由自动配置类创建和配置相应的 Bean
详细解析
自动配置的核心流程:
@EnableAutoConfiguration 注解触发自动配置机制
@Import 导入 AutoConfigurationImportSelector:
- 通过
SpringFactoriesLoader.loadFactoryNames()方法 - 读取所有 jar 包中
META-INF/spring.factories文件 - 文件中以
EnableAutoConfiguration为 key 的配置类列表
- 通过
@Conditional 条件过滤:
| 条件注解 | 作用 |
|---|---|
| @ConditionalOnClass | 类路径存在指定类时生效 |
| @ConditionalOnMissingClass | 类路径不存在指定类时生效 |
| @ConditionalOnBean | 容器中存在指定 Bean 时生效 |
| @ConditionalOnMissingBean | 容器中不存在指定 Bean 时生效 |
| @ConditionalOnProperty | 指定属性满足条件时生效 |
- 自动配置类创建 Bean:
- 过滤后的配置类被加载到 Spring 容器
- 配置类中的
@Bean方法创建相应的组件 - 同时可以通过
application.properties进行属性定制
自动配置的本质:
- 开发者无需手动编写大量的配置代码
- Spring Boot 根据项目的依赖自动完成配置
- 可以通过配置文件覆盖自动配置的默认值
面试口头回答
SpringBoot 自动配置的原理主要基于 @SpringBootApplication 中的 @EnableAutoConfiguration 注解。
具体流程是:
- 首先,@EnableAutoConfiguration 通过 @Import 导入 AutoConfigurationImportSelector 配置选择器;
- 然后,这个选择器会读取所有 jar 包中 META-INF/spring.factories 文件,获取里面以 EnableAutoConfiguration 为 key 的自动配置类列表;
- 接着,通过 @Conditional 系列条件注解对这些配置类进行过滤,比如 @ConditionalOnClass 判断类路径中是否存在某个类,@ConditionalOnProperty 判断配置文件中是否启用了某个属性;
- 最后,满足条件的自动配置类会被加载到 Spring 容器,创建和配置相应的 Bean。
这样开发者就不需要手动写大量的配置代码,SpringBoot 会根据项目中引入的依赖自动完成配置。比如引入了 spring-boot-starter-web,就会自动配置 Tomcat、SpringMVC 等组件。同时我们也可以通过 application.properties 或 application.yml 来覆盖自动配置的默认值。
六、Spring 事务
6.1 Spring 事务原理与失效场景
核心要点
- @Transactional 本质是通过 AOP 生成代理对象增强原方法
- 方法执行前开启事务,成功执行完毕提交,出现异常回滚
- 失效场景:异常被吞、抛出编译异常、非 public 方法、内部 this 调用、数据源不归 Spring 管
详细解析
@Transactional 的本质:
- 将被事务注解标注的方法交给 Spring 进行事务管理
- 本质是通过 AOP 生成代理对象增强原方法
- 方法执行前开启事务;成功执行完毕提交事务;出现异常回滚事务
Spring 事务失效的常见场景:
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 异常被 try-catch 捕获 | 事务通知需要捕获抛出的异常才能回滚 | 在 catch 块手动抛出 RuntimeException |
| 抛出编译时异常 | Spring 默认只回滚 RuntimeException | 配置 rollbackFor = Exception.class |
| 非 public 方法 | Spring 事务为方法创建代理的前提是 public | 将方法改为 public |
| 内部 this 调用 | 通过 this.method() 不走代理对象 | 注入自身代理对象或拆分到另一个类 |
| 数据源不归 Spring 管 | 事务管理器无法管理非 Spring 管理的数据源 | 确保使用 Spring 管理的 DataSource |
事务传播行为(Propagation):
| 传播行为 | 说明 |
|---|---|
| REQUIRED(默认) | 当前有事务则加入,没有则新建 |
| SUPPORTS | 当前有事务则加入,没有则以非事务执行 |
| REQUIRES_NEW | 新建事务,挂起当前事务 |
| NOT_SUPPORTED | 以非事务执行,挂起当前事务 |
| NEVER | 以非事务执行,当前有事务则抛异常 |
| MANDATORY | 当前必须有事务,否则抛异常 |
| NESTED | 在当前事务中创建嵌套事务 |
事务隔离级别:
| 隔离级别 | 问题 | 说明 |
|---|---|---|
| DEFAULT | - | 使用数据库默认隔离级别 |
| READ_UNCOMMITTED | 脏读、不可重复读、幻读 | 最低级别 |
| READ_COMMITTED | 不可重复读、幻读 | Oracle 默认 |
| REPEATABLE_READ | 幻读 | MySQL 默认 |
| SERIALIZABLE | 无 | 最高级别,性能最差 |
事务 ID 的生成时机:
- 在可重复读隔离级别下,事务 ID 是在目标方法执行之前生成的
- Spring 通过 AOP 代理拦截方法调用,事务管理器在方法执行前创建新事务并分配唯一事务 ID
- 事务 ID 绑定到当前线程,贯穿整个方法执行过程
面试口头回答
Spring 事务主要通过 @Transactional 注解实现声明式事务管理。
本质是通过 AOP 生成代理对象来增强原方法。方法执行前开启事务,成功执行完提交,如果出现异常就回滚。Spring 支持事务传播行为和隔离级别,能灵活控制事务嵌套和并发访问。
事务失效的场景我总结为几个:
- 异常被自己 try-catch 吞掉了。事务通知只有捕捉到抛出的异常才能回滚,如果自己在方法里 catch 了没抛出去,事务就感知不到异常。解决方式是在 catch 块里手动抛出 RuntimeException。
- 抛出的是编译时异常。Spring 默认只回滚 RuntimeException,如果抛出的是 IOException 这类编译异常,事务不会回滚。可以通过配置 rollbackFor = Exception.class 来解决。
- 方法不是 public 的。Spring 事务为方法创建代理的前提是方法必须是 public,private 或 protected 方法事务不生效。
- 内部 this 调用。比如类内部方法 A 调用方法 B,B 上有 @Transactional,但通过 this.methodB() 调用不会走代理对象,事务就失效了。解决方式是注入自身的代理对象,或者把方法拆到另一个类里。
我在项目中遇到最多的是异常被吞和内部调用导致的事务失效问题,排查时一般会先检查方法是不是 public,再看异常有没有正确抛出,最后确认调用是否通过代理对象。
七、Spring、SpringMVC、SpringBoot 的关系
7.1 三者的区别与关系
核心要点
- Spring:轻量级 IoC 和 AOP 容器,解决对象管理和解耦问题
- SpringMVC:Spring 的 Web 层框架,实现 MVC 模式
- SpringBoot:基于 Spring + SpringMVC 的快速开发框架,提供自动配置、内嵌服务器、starter 依赖
- 关系:Spring 是核心,SpringMVC 是 Web 模块,SpringBoot 是对全家桶的整合和简化
详细解析
Spring:
- 轻量级的 IoC 和 AOP 容器
- 主要解决对象管理和解耦问题
- 提供事务管理、数据访问、消息传递等企业级功能
SpringMVC:
- Spring 提供的 Web 层框架
- 实现了 MVC(Model-View-Controller)模式
- Model:模型数据,通常在控制器方法中填充
- View:视图,用于生成用户界面
- Controller:控制器,接收用户请求,调用业务逻辑,返回处理结果
SpringBoot:
- 基于 Spring 和 SpringMVC 的快速开发框架
- 提供自动配置、内嵌服务器(Tomcat/Jetty/Undertow)和 starter 依赖
- 极大简化了项目搭建和配置
- 约定大于配置的理念
三者关系:
Spring(核心 IoC + AOP)
└── SpringMVC(Web 层模块)
└── SpringBoot(快速开发框架,整合简化)
面试口头回答
Spring、SpringMVC 和 SpringBoot 是层层递进的关系。
Spring 是核心框架,它是一个轻量级的 IoC 和 AOP 容器,主要解决对象管理和解耦问题。提供了依赖注入、事务管理、AOP 等企业级功能。
SpringMVC 是 Spring 的 Web 层框架,实现了 MVC 模式。Model 是模型数据,View 是视图用于展示,Controller 是控制器接收请求、调用业务逻辑、返回结果。
SpringBoot 是基于 Spring 和 SpringMVC 的快速开发框架,它提供了自动配置、内嵌服务器和 starter 依赖管理。开发者不需要写大量的 XML 配置,引入 starter 就能自动集成相关组件,极大简化了项目搭建。
三者的关系是:Spring 是核心,SpringMVC 属于 Spring 家族的 Web 模块,SpringBoot 是对 Spring 全家桶的进一步整合和简化。我在项目中直接使用 SpringBoot,它底层已经包含了 Spring 和 SpringMVC,只需要引入对应的 starter 就可以快速搭建 Web 应用。
八、SpringMVC 执行流程
8.1 SpringMVC 执行流程
核心要点
- 核心组件:DispatcherServlet(前端控制器)、HandlerMapping(处理器映射器)、HandlerAdapter(处理器适配器)、Controller(后端处理器)、ViewResolver(视图解析器)
- 流程:请求 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ModelAndView → ViewResolver → View → 响应
详细解析
核心组件说明:
| 组件 | 作用 |
|---|---|
| DispatcherServlet | 前端控制器,接收所有请求并分发 |
| HandlerMapping | 处理器映射器,根据 URL 找到对应的 Controller 方法 |
| HandlerAdapter | 处理器适配器,执行 Controller 方法 |
| Controller | 后端处理器,处理业务逻辑 |
| ViewResolver | 视图解析器,将逻辑视图名解析为具体视图 |
| View | 视图,负责渲染页面 |
执行流程:
- 用户发送请求 → 请求到达 DispatcherServlet(前端控制器)
- DispatcherServlet 调用 HandlerMapping(处理器映射器),根据请求 URL 找到对应的 Controller 方法
- HandlerMapping 返回 处理器对象及拦截器给 DispatcherServlet
- DispatcherServlet 调用 HandlerAdapter(处理器适配器)
- HandlerAdapter 调用后端处理器 Controller,执行业务逻辑
- Controller 返回 ModelAndView 对象(包含模型数据和逻辑视图名)
- HandlerAdapter 将 ModelAndView 返回给 DispatcherServlet
- DispatcherServlet 传给 ViewResolver(视图解析器)进行视图解析
- ViewResolver 解析返回视图 View 给 DispatcherServlet
- DispatcherServlet 渲染视图 View 并响应用户
RESTful API 场景简化:
- 如果 Controller 使用了
@ResponseBody或@RestController - Controller 直接返回数据对象(如 JSON)
- 跳过 ViewResolver 和 View 渲染步骤
- 由 HttpMessageConverter 将对象转换为响应体
面试口头回答
SpringMVC 的执行流程我按步骤来说:
第一步,用户发送请求,请求首先到达 DispatcherServlet,它是前端控制器,所有请求都经过它。
第二步,DispatcherServlet 调用 HandlerMapping 处理器映射器,根据请求的 URL 找到对应的 Controller 方法。
第三步,HandlerMapping 返回处理器对象和拦截器给 DispatcherServlet。
第四步,DispatcherServlet 调用 HandlerAdapter 处理器适配器。
第五步,HandlerAdapter 调用后端处理器 Controller 执行业务逻辑。
第六步,Controller 返回 ModelAndView 对象,包含模型数据和逻辑视图名。
第七步,HandlerAdapter 把 ModelAndView 返回给 DispatcherServlet。
第八步,DispatcherServlet 传给 ViewResolver 视图解析器,把逻辑视图名转换为具体的视图。
第九步,ViewResolver 返回视图给 DispatcherServlet。
第十步,DispatcherServlet 渲染视图,把数据填充进去,生成最终响应返回给用户。
如果是开发 RESTful API,使用了 @RestController 或 @ResponseBody,Controller 直接返回数据对象,Spring 会自动用 HttpMessageConverter 转成 JSON,就不需要视图解析和渲染这些步骤了。
九、Spring 设计模式与实战
9.1 Spring 中用到了哪些设计模式?
核心要点
- 工厂模式:BeanFactory、ApplicationContext 创建 Bean
- 单例模式:Spring 默认 Bean 作用域
- 代理模式:AOP 中的 JDK 动态代理和 CGLIB
- 模板方法模式:JdbcTemplate、RestTemplate
- 观察者模式:ApplicationListener 事件监听
- 适配器模式:HandlerAdapter 适配不同类型的处理器
- 策略模式:Resource 接口的不同资源加载策略
- 装饰器模式:BeanWrapper、BufferedReader
面试口头回答
Spring 中用到的设计模式非常多,我说几个主要的:
工厂模式:Spring 的 BeanFactory 和 ApplicationContext 就是工厂模式的应用,负责创建和管理 Bean 实例。
单例模式:Spring 默认的 Bean 作用域就是单例,容器中只有一个实例,这是单例模式的典型应用。
代理模式:Spring AOP 的实现就用了代理模式,包括 JDK 动态代理和 CGLIB 代理。
模板方法模式:JdbcTemplate、RestTemplate、JpaTemplate 这些都是模板方法模式,定义了算法的骨架,具体的实现由子类或回调完成。
观察者模式:Spring 的事件机制就是观察者模式,ApplicationEvent 发布事件,ApplicationListener 监听处理。
适配器模式:SpringMVC 中的 HandlerAdapter 就是适配器模式,把不同类型的处理器适配成统一的接口。
策略模式:Resource 接口对不同类型的资源(URL、File、Classpath)采用不同的加载策略。
这些设计模式让 Spring 具有很高的扩展性和灵活性。
9.2 如何用 Spring Boot 快速实现一个 HTTP 服务?
核心要点
- 创建 Spring Boot 项目,引入 spring-boot-starter-web
- 主启动类使用 @SpringBootApplication
- 编写 REST 控制器,使用 @RestController 和 @GetMapping
- 启动应用后通过浏览器或 curl 访问
面试口头回答
用 Spring Boot 快速实现一个 HTTP 服务很简单:
第一步,创建一个 Spring Boot 项目,引入 spring-boot-starter-web 依赖。
第二步,编写主启动类,加上 @SpringBootApplication 注解,在 main 方法里调用 SpringApplication.run 启动应用。
第三步,编写 REST 控制器类,加上 @RestController 注解,在里面写一个方法,加上 @GetMapping("/hello") 映射接口路径,方法返回 “Hello World”。
第四步,启动应用,Spring Boot 会自动内嵌启动 Tomcat。
第五步,通过浏览器访问 http://localhost:8080/hello,或者用 curl 测试,就能看到 Hello World 的响应。
整个过程不需要配置 web.xml,不需要部署到外部 Tomcat,Spring Boot 的自动配置和约定大于配置的理念让开发非常高效。
9.3 如何设计并编写一个 Spring Boot 的 Controller?
核心要点
- 使用 @RestController 定义控制器
- 使用 @RequestMapping、@GetMapping、@PostMapping 指定接口路径和方法类型
- 使用 @PathVariable、@RequestParam、@RequestBody 获取请求参数
- Controller 只负责请求/响应转换,业务逻辑委托给 Service 层
- 返回对象或 ResponseEntity,Spring Boot 自动序列化为 JSON
- 结合 @Valid 参数校验、@ControllerAdvice 全局异常处理提高健壮性
面试口头回答
设计 Spring Boot 的 Controller 我遵循这几个原则:
第一,使用 @RestController 定义控制器类,它是 @Controller 和 @ResponseBody 的组合,这样类里所有方法的返回值都会自动转成 JSON。
第二,使用 @GetMapping、@PostMapping 这些注解来定义接口路径和请求方法类型。比如 @GetMapping("/users/{id}") 处理 GET 请求,@PostMapping("/users") 处理 POST 请求。
第三,参数获取根据场景选择不同注解:
- @PathVariable 从 URL 路径中取参数,比如 /users/123 中的 123;
- @RequestParam 从 URL 查询参数中取,比如 ?name=xxx;
- @RequestBody 接收请求体中的 JSON 数据,转为 Java 对象。
第四,Controller 只负责请求和响应的转换,业务逻辑全部委托给 Service 层处理,保持 Controller 的简洁。
第五,返回数据可以直接返回对象,Spring Boot 会自动用 Jackson 序列化为 JSON。如果需要控制 HTTP 状态码,可以用 ResponseEntity 包装返回结果。
第六,健壮性方面,可以结合 @Valid 做参数校验,用 @ControllerAdvice 做全局异常处理,统一返回错误响应。
我在项目中的典型 Controller 结构是:类上加 @RestController 和 @RequestMapping("/api/users"),方法上用具体的请求映射注解,参数用 DTO 对象配合 @RequestBody 和 @Valid,方法体内调用 Service 处理业务,最后返回统一封装的 Result 对象。
评论区