【Spring框架】——原理篇
Spring框架核心解析
本文将深入探讨Spring框架中最为核心和常见的面试题,旨在帮助开发者构建系统性的知识体系,从容应对技术面试。内容如下:
一、Spring-单例Bean是线程安全的吗?
这是一个经典的面试题,答案是:默认情况下,Spring框架中的单例Bean不是线程安全的。
- 成员方法值是可变的,需要考虑线程安全
- 成员变量是没有线程安全的,因为他是无状态的类(service和DAO),无状态就是判断这个成员变量能不能被修改,而他们不能被修改,所以没有线程安全。这是说明了在依赖注入时为什么推荐使用构造方法注入。
- 方法参数是局部局部变量,之后不会改变,使用没有线程安全的顾虑
原因分析: Spring的IoC容器默认会管理单例(Singleton)作用域的Bean。这意味着容器中每个Bean定义只存在一个共享的实例。当多个线程同时访问这个单例Bean时,如果该Bean包含可变的成员变量(状态),并且没有采取任何同步措施,就会发生线程安全问题(如数据覆盖、脏读、不可重复读等)。
示例:
@Service
public class UnsafeCounterService {private int count = 0; // 可变的状态public void increment() {count++; // 非原子操作,线程不安全}public int getCount() {return count;}
}
多个线程同时调用 increment()
方法会导致最终的 count
值小于预期。
如何保证线程安全?
- 无状态Bean(首选方案):将Bean设计为无状态的,即不包含任何可变的成员变量。所有操作都通过局部变量或参数来完成。这是最推荐的方式,因为无状态Bean本质就是线程安全的。
- 使用同步机制:在方法或代码块上加
synchronized
关键字,或者使用ReentrantLock
。这会带来性能开销,且需要仔细设计锁的粒度。 - 使用ThreadLocal:将可变状态封装到
ThreadLocal
中,这样每个线程都会拥有该变量的一个副本。适用于与线程绑定的数据(如用户会话信息)。 - 改变Bean的作用域:将Bean的作用域改为
prototype
(原型),这样每次请求都会创建一个新的实例,自然不存在共享问题。但这通常不是解决此类问题的最佳实践,因为它会创建大量对象,增加GC压力。
结论:Spring不负责处理单例Bean的线程安全问题,这需要开发者根据业务场景自行保证。编写无状态的Bean是最佳实践。
二、Spring-AOP场景
1. AOP的核心概念
增强(Advice),在Spring AOP的官方中文文档中常被直译为“通知”,但“增强”这个词更能体现其本质含义。
简单来说:增强就是你想要注入到目标类连接点(特定方法)上的那段“增强”功能的代码。
- Aspect(切面):封装横切关注点的类,包含 Advice 和 Pointcut。
- Joinpoint(连接点):程序执行过程中明确的点,如方法调用、异常抛出。在Spring AOP中,连接点总是代表方法的执行。
- Advice(通知):切面在特定连接点执行的动作。类型包括:
@Before
:前置通知@AfterReturning
:返回后通知@AfterThrowing
:异常通知@After
:后置通知(无论成功与否)@Around
:环绕通知(功能最强大,可以控制是否执行目标方法)
- Pointcut(切点):匹配连接点的表达式,定义了通知何时被执行。
- Weaving(织入):将切面应用到目标对象并创建代理对象的过程。
2. Spring AOP的实现原理 Spring AOP默认使用动态代理:
- 如果目标对象实现了接口,则使用 JDK 动态代理。
- 如果目标对象没有实现任何接口,则使用 CGLIB 字节码生成。
可以通过配置 @EnableAspectJAutoProxy(proxyTargetClass = true)
强制使用CGLIB。
3. AOP的应用场景
- 日志记录
- 事务管理(
@Transactional
的本质就是AOP) - 安全认证和授权
- 性能监控(方法执行时间)
- 全局异常处理
- 缓存
一、日志记录
1、comtroller类
2、aop类
3、自定义注解
二、事务管理
小结:
三、Spring-事务失效的场景
非检查异常:Runtime异常
事务失效是开发中常见的坑,主要场景如下:
- 事务方法非public修饰:
@Transactional
只能用于 public 方法上,应用于其他方法时,事务不会失效,但也不会报错。 - 自调用(Same Class Invocation):类内部的方法调用该类的事务方法,如
this.methodB()
。这是因为事务基于代理,自调用绕过了代理对象。- 解决方案:注入自身的代理对象
@Autowired private MyService self;
然后调用self.methodB()
。
- 解决方案:注入自身的代理对象
- 异常被捕获未抛出:事务管理依赖于异常来回滚。如果在方法中捕获了异常 (
try-catch
) 却没有重新抛出 (throw new RuntimeException(e)
),事务将不会回滚。 - 抛出的异常类型错误:默认情况下,
@Transactional
只在遇到运行时异常 (RuntimeException
) 和错误 (Error
) 时回滚。遇到受检异常 (Exception
) 不会回滚。- 解决方案:使用
@Transactional(rollbackFor = Exception.class)
指定回滚的异常类型。
- 解决方案:使用
- 数据源未配置事务管理器:必须配置
PlatformTransactionManager
Bean(如DataSourceTransactionManager
),否则事务不生效。 - 在非Spring管理的类中使用:例如,在普通的new出来的对象中使用
@Transactional
注解是无效的。 - 数据库引擎不支持事务:如MySQL的MyISAM引擎不支持事务,需要改用InnoDB引擎。
四、Spring-Bean的生命周期
代码演示:
Spring Bean的生命周期非常复杂,但时主要掌握以下核心步骤:
- 实例化(Instantiate):通过构造器或工厂方法创建Bean实例。
- 属性赋值(Populate):为Bean的属性注入值(依赖注入)。
- BeanPostProcessor前置处理:调用
BeanPostProcessor.postProcessBeforeInitialization()
方法。 - 初始化(Initialize):
- 如果Bean实现了
InitializingBean
接口,则调用afterPropertiesSet()
方法。 - 如果Bean配置了自定义的
init-method
(或@PostConstruct
注解),则调用此方法。
- 如果Bean实现了
- BeanPostProcessor后置处理:调用
BeanPostProcessor.postProcessAfterInitialization()
方法(AOP代理对象通常在此阶段生成)。 - Bean就绪:此时Bean已被完全初始化,存放在Spring容器中,可以被应用程序使用。
- 销毁(Destroy):
- 容器关闭时,如果Bean实现了
DisposableBean
接口,则调用destroy()
方法。 - 如果配置了自定义的
destroy-method
(或@PreDestroy
注解),则调用此方法。
- 容器关闭时,如果Bean实现了
简化记忆:实例化 -> 属性填充 -> (前置处理) -> 初始化 -> (后置处理/AOP) -> 使用 -> 销毁
五、Spring-Bean的循环依赖(循环引用)
循环依赖是指两个或多个Bean相互持有对方,形成了闭环依赖。例如:A依赖B,B也依赖A。
Spring如何解决循环依赖?(基于三级缓存) Spring通过三级缓存机制巧妙地解决了单例Bean通过setter方法注入的循环依赖问题。
三级缓存:
- singletonObjects(一级缓存):存放已经完全初始化好的单例Bean。
- earlySingletonObjects(二级缓存):存放提前曝光的、早期的Bean对象(已实例化但未初始化)。
- singletonFactories(三级缓存):存放Bean的ObjectFactory,用于生成早期引用。
解决流程(以A和B循环依赖为例):
- 创建A,实例化A,将A的ObjectFactory放入三级缓存。
- 为A注入属性时,发现需要B。
- 开始创建B,实例化B,将B的ObjectFactory放入三级缓存。
- 为B注入属性时,发现需要A。此时从三级缓存中拿到A的ObjectFactory,并调用
getObject()
方法获取A的早期引用(可能是一个代理对象)。将A的早期引用放入二级缓存,并从三级缓存移除。B成功注入A。 - B完成属性填充、初始化,成为一个完整的Bean,放入一级缓存,并清除二、三级缓存。
- A接着注入B,完成后续的初始化步骤。A完成后也放入一级缓存。
一级缓存
二级缓存
A对象放到SingletonObjects后earlysingletonobject的原始A对象就会消失。
但是如果刚开始A是代理对象,而最后存储到单例池的却不是代理对象,所以会用到三级缓存
三级缓存
无法解决的循环依赖场景:
三级缓存可以去解决初始化过程中的循环依赖,不能解决构造函数产生的循环依赖
@lazy:延迟加载,意思就是什么时候需要对象,再进行ban对象的创建,并不是说在去实例化的时候直接把对象给注入进来
- 构造器注入的循环依赖:因为实例化Bean时需要构造器参数,而另一个Bean还未创建,无法提前曝光,会抛出
BeanCurrentlyInCreationException
。 - 多例(Prototype)作用域的Bean循环依赖:Spring不管理多例Bean的完整生命周期,无法进行曝光和缓存。
小结:
要点:1、三级缓存就已经包含了,一二级缓存,直接解释三级缓存就行了
2、三级缓存中为什么还需要二级缓存,因为对象都是单例的,都通过三级缓存创建的话可能会产生多例对象
3.pring解决的主要都是set注入:set注入是构造函授调用之后才完成的,循环依赖当然可以解决 因为对象都创建好了 只是算个半成品
懒加载就是先不去加载B,先把A对象创建出来,等到真正使用B的时候再去实例化B,,这是A已经实例化完毕了
六、SpringMVC-执行流程
视图阶段:
handler:解释当前某一个控制器的某一个方法
前后端分离
相当于化简了ModelAndView阶段,因为前后端分离不用去视图解析
SpringMVC的核心流程清晰且经典,其核心是 DispatcherServlet
。
七、SpringBoot-自动配置原理
SpringBoot的自动配置是其核心魔法,其原理基于 @SpringBootApplication
注解和条件化配置。
入口注解:@SpringBootApplication 它是一个复合注解,包含:
@SpringBootConfiguration
:表明是配置类。@ComponentScan
:组件扫描。@EnableAutoConfiguration
:开启自动配置的核心。
@EnableAutoConfiguration
的作用 它利用@Import
导入了AutoConfigurationImportSelector
。AutoConfigurationImportSelector
这个类的核心方法是selectImports()
,它会从 classpath 下的META-INF/spring.factories
文件中读取所有EnableAutoConfiguration
键对应的自动配置类全名(如org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
)。自动配置类(XxxAutoConfiguration) 这些配置类是自动配置的蓝本。它们使用
@Configuration
声明,并利用大量的@ConditionalOnXxx
条件注解(如@ConditionalOnClass
,@ConditionalOnProperty
,@ConditionalOnMissingBean
)来进行条件化配置。@ConditionalOnClass(DataSource.class)
:表示类路径下存在DataSource
类(即引入了数据库驱动)时,配置才生效。@ConditionalOnMissingBean(DataSource.class)
:表示容器中不存在DataSource
Bean时,才自动配置一个默认的。
总结流程:
- SpringBoot启动时加载
META-INF/spring.factories
文件中的自动配置类。 - 根据当前项目的环境、类路径、已有Bean等条件(
@ConditionalOnXxx
),决定哪些配置类生效。 - 生效的配置类会向容器中添加预先定义好的Bean。
- “约定大于配置”:开发者只需引入starter依赖,SpringBoot就会自动配置好大部分组件。如果想自定义,只需自己声明一个Bean即可覆盖默认配置。
- SpringBoot启动时加载
八、Spring框架常见的注解
类别 | 注解 | 说明 |
---|---|---|
Spring核心 | @Component | 通用组件注解,被其标注的类会被组件扫描。 |
@Repository | 标注DAO层组件。 | |
@Service | 标注Service层业务逻辑组件。 | |
@Controller | 标注Web层控制器组件。 | |
@Autowired | 自动依赖注入(按类型)。 | |
@Qualifier | 与 @Autowired 配合,按名称注入。 | |
@Value | 注入普通属性或SpEL表达式。 | |
@Configuration | 声明一个类为配置类。 | |
@Bean | 在配置类中声明一个Bean。 | |
@Scope | 指定Bean的作用域(singleton, prototype等)。 | |
@Profile | 指定配置或Bean在哪个环境下激活。 | |
@PostConstruct | 标注初始化方法。 | |
@PreDestroy | 标注销毁方法。 | |
Spring AOP | @Aspect | 声明一个切面。 |
@Pointcut | 声明一个切点表达式。 | |
@Before /@After / etc. | 声明通知。 | |
Spring事务 | @Transactional | 声明式事务管理。 |
SpringMVC | @RequestMapping | 映射Web请求(路径、方法等)。 |
@GetMapping /@PostMapping | @RequestMapping 的简写。 | |
@RequestParam | 获取请求参数。 | |
@PathVariable | 获取URL路径中的变量。 | |
@RequestBody | 获取请求体,反序列化为对象。 | |
@ResponseBody | 将方法返回值直接写入响应体。 | |
@RestController | @Controller + @ResponseBody 。 | |
@ExceptionHandler | 声明异常处理方法(Controller内)。 | |
@ControllerAdvice | 全局异常处理。 | |
SpringBoot | @SpringBootApplication | 核心启动注解。 |
@EnableAutoConfiguration | 开启自动配置。 | |
@ConditionalOnXxx | 一系列条件注解,用于自动配置。 |
希望这篇全面解析能帮助您更好地理解和掌握Spring框架的核心知识。