Spring FAQ
1. Spring 如何解决循环依赖的问题?
三级缓存, 并提前暴露 Bean
在 Spring 中,循环依赖是指两个或多个 bean 相互依赖,形成依赖闭环。例如,BeanA 依赖 BeanB,而 BeanB 又依赖 BeanA。Spring 通过三级缓存机制 (三级缓存) 来解决这种循环依赖问题。
Spring 的三级缓存机制
Spring 使用三级缓存机制来解决循环依赖。三级缓存分为以下三个层次:
一级缓存 (singletonObjects) :用于存放完全初始化好的单例 bean。这个缓存中存储的是已经完成初始化的 bean,之后直接从这里获取 bean 实例。
二级缓存 (earlySingletonObjects) :用于存放早期暴露的单例对象,通常是已经实例化但未完成属性注入和初始化的 bean 实例。这一缓存中的 bean 是可以被代理的,以便在循环依赖中注入代理对象。
三级缓存 (singletonFactories) :用于存放可以创建早期 bean 的工厂对象。这个缓存中的对象是 ObjectFactory 类型的 lambda 表达式或方法引用,它们负责生成 bean 实例,并将其暴露给二级缓存。
三级缓存的工作流程
假设 BeanA 和 BeanB 存在循环依赖,Spring 解决该问题的流程如下:
实例化:Spring 在创建 BeanA 时,首先会在 singletonObjects (一级缓存) 中查找 BeanA。如果找不到,Spring 会先实例化 BeanA,但不会立即填充它的依赖属性。
暴露早期对象:Spring 将 BeanA 的 ObjectFactory 放入三级缓存中 (singletonFactories) 。ObjectFactory 是一个 lambda 表达式或方法引用,用于生成 BeanA 的代理对象。此时,BeanA 尚未完成初始化,但可以提前暴露其引用。
创建依赖的 BeanB:在填充 BeanA 的依赖属性时,发现 BeanA 依赖 BeanB。因此,Spring 开始创建 BeanB,并执行同样的步骤,将 BeanB 的 ObjectFactory 暴露到三级缓存中。
检测循环依赖:在创建 BeanB 时,发现 BeanB 依赖 BeanA。Spring 在二级缓存 (earlySingletonObjects) 和三级缓存中查找 BeanA。此时,BeanA 的 ObjectFactory 已经在三级缓存中存在,因此 Spring 调用 ObjectFactory 获取 BeanA 的代理对象,并将该代理对象放入二级缓存 (earlySingletonObjects) 。
完成依赖注入:BeanB 获取到 BeanA 的早期对象 (代理对象) ,并成功完成初始化。然后,Spring 将 BeanB 从三级缓存转移到一级缓存 (singletonObjects) 中。
完成 BeanA 的初始化:回到 BeanA 的初始化,BeanA 的依赖 (BeanB) 已经完成注入,Spring 继续完成 BeanA 的初始化,并将 BeanA 放入一级缓存中。
通过上述流程,Spring 成功解决了 BeanA 和 BeanB 的循环依赖。
需要注意的几点
三级缓存的使用:三级缓存的引入是为了支持代理机制,即提前暴露不完整的 bean。二级缓存存放的是已经实例化但尚未完全初始化的对象。三级缓存则用于存放对象工厂,以便在需要时创建早期代理对象。
构造器注入无法解决循环依赖:Spring 的三级缓存机制只能解决属性注入的循环依赖。对于构造器注入的循环依赖,Spring 无法解决,因为构造器注入需要在实例化时完全提供依赖对象。
可以通过 @Lazy 注解延迟加载:通过将依赖标记为 @Lazy,Spring 可以推迟实例化依赖的对象,从而避免循环依赖问题。
代理对象在循环依赖中的作用:在一些场景中,Spring 会使用 CGLIB 动态代理技术生成代理对象,这样可以提前暴露不完整的 bean,以便完成依赖注入。
2. Spring Boot 的自动装配过程是如何实现的?
spring boot 启动时会通过 @EnableAutoConfiguration 找到 META-INF/spring.factories 文件中的自动配置类 AutoConfiguration,然后根据条件加载这些类,从而完成自动配置。
启动类和 @SpringBootApplication
@SpringBootApplication 是复合注解,包含 @EnableAutoConfiguration、@ComponentScan 和 @Configuration。
@EnableAutoConfiguration 启用自动配置,扫描 META-INF/spring.factories 文件中的自动配置类。
@EnableAutoConfiguration 和 spring.factories
@EnableAutoConfiguration 配合 SpringFactoriesLoader 读取 spring-boot-autoconfigure JAR 包中的 META-INF/spring.factories 文件。
spring.factories 列出了所有自动配置类,它们在 Spring 容器启动时被逐个加载并处理。
条件注解 @Conditional
自动配置类通常使用 @Conditional 系列注解 (如 @ConditionalOnMissingBean、@ConditionalOnClass) 来控制配置的生效。
这些条件注解确保在满足特定条件 (如类存在、Bean 不存在) 时才加载相应的 Bean。
自动配置类的加载与执行
Spring Boot 将所有符合条件的自动配置类加载到应用上下文中。
自动配置类定义了许多 @Bean 方法来创建和配置组件,这些 Bean 会自动注入到 Spring 应用上下文中。
自定义配置与优先级
用户自定义的配置 (例如 application.properties) 会覆盖默认的自动配置。
自定义 @Configuration 类或 @Bean 方法会优先于自动配置类中的默认 Bean 定义。
总结
Spring Boot 自动装配依赖于 @EnableAutoConfiguration、spring.factories 文件和 @Conditional 注解的组合,通过条件化加载 Bean,确保应用按需配置并减少手动配置。
3. Spring 中创建的 Bean 是单例还是多例?
在 Spring 中,Bean 的作用域 (scope) 可以设置为 “singleton”、”prototype” 或其他。默认情况下,如果没有指定作用域,Spring 将使用单例模式创建 Bean。
4. Spring IoC 的原理是什么?
Spring IoC (Inversion of Control,控制反转) 的原理基于依赖注入 (Dependency Injection, DI) ,通过配置文件或注解将对象的依赖交由 Spring 容器管理,实现对象之间的松耦合。其核心流程如下:
Bean 定义与扫描
Bean 定义:Spring 使用 XML 文件或注解 (如 @Component、@Service、@Controller) 定义 Bean。
扫描与注册:Spring 扫描应用中的配置类或指定包,将符合条件的类注册为 Bean。
BeanFactory 和 ApplicationContext
BeanFactory:提供最基本的 IoC 容器功能,通过懒加载管理 Bean 的创建和依赖注入。
ApplicationContext:扩展自 BeanFactory,提供更丰富的功能,如事件发布、国际化支持和自动扫描。
Bean 的生命周期
实例化:Spring 使用反射实例化 Bean。
依赖注入:Spring 通过构造器、Setter 或字段注入,将所需依赖注入到 Bean 中。
初始化:调用 @PostConstruct、InitializingBean 等方法,完成 Bean 的初始化。
销毁:在容器关闭时调用 @PreDestroy、DisposableBean 等方法销毁 Bean。
依赖注入 (DI)
构造器注入:通过构造函数完成依赖注入,适合不可变依赖。
Setter 注入:通过 Setter 方法完成依赖注入,适合可选依赖。
字段注入:通过 @Autowired 等注解直接注入字段,简化依赖管理。
代理与 AOP 支持
Spring 使用动态代理生成 Bean 的代理对象,支持 AOP 功能 (如事务、日志等) ,进一步增强 Bean 的功能。
总结
Spring IoC 容器通过依赖注入管理 Bean 的生命周期和依赖关系,简化了对象间的协作和配置,实现了松耦合、可扩展的应用架构。
5. Spring Bean 的生命周期是怎样的?
Spring Bean 的生命周期包含实例化、依赖注入、初始化、销毁等阶段,可以通过实现接口 Lifecycle 或 BeanPostProcessor 等方式来定制生命周期。
Spring Bean 的生命周期是指从实例化到销毁的整个过程,主要分为以下几个阶段:
实例化 (Instantiation)
Spring 容器根据 Bean 定义使用反射机制创建 Bean 实例,但此时还未初始化依赖。
属性注入 (Populate Properties)
Spring 为 Bean 注入依赖 (DI) ,完成属性值的设置。这可以通过构造器、Setter 方法或直接字段注入实现。
初始化 (Initialization)
BeanNameAware、BeanFactoryAware 等接口:如果 Bean 实现了这些接口,Spring 会调用相应的回调方法,将容器相关信息传递给 Bean。
BeanPostProcessor 前置处理:在 Bean 初始化方法前,Spring 会调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法。
初始化方法:
如果实现了 InitializingBean 接口,会调用 afterPropertiesSet 方法。
如果定义了 init-method 或使用了 @PostConstruct 注解,则会执行对应的初始化方法。
BeanPostProcessor 后置处理:在 Bean 初始化后,Spring 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法,可能返回增强后的 Bean (如 AOP 代理) 。
使用阶段
Bean 完成初始化后,处于就绪状态,可以在应用中被使用。它在 Spring 容器中存续,直到容器销毁。
销毁 (Destruction)
当容器关闭时,Spring 会对单例作用域的 Bean 执行销毁操作。
销毁方法:
如果实现了 DisposableBean 接口,会调用其 destroy 方法。
如果定义了 destroy-method 或使用了 @PreDestroy 注解,则会执行相应销毁方法。
生命周期流程总结图
实例化 -> 属性注入 -> 初始化前处理 -> 初始化 -> 初始化后处理 -> 使用 -> 销毁
总结
Spring Bean 的生命周期由容器管理,通过回调接口和注解实现定制化的初始化和销毁操作,使 Bean 的管理更灵活和可扩展。
6. Spring 依赖注入有哪几种方式
注入方式 |
优点 |
缺点 |
适用场景 |
|---|---|---|---|
构造器注入 |
强制依赖完整性,适合不可变依赖 |
代码冗长 |
必须的依赖 |
Setter 注入 |
灵活,适合可选依赖 |
依赖完整性不保证 |
可选的、可变的依赖 |
字段注入 |
简洁,代码量少 |
不利于测试和解耦 |
简单场景 |
通常推荐优先使用 构造器注入,其次是 Setter 注入,字段注入则仅适用于简单场景。
7. Spring Bean 的作用域有哪些
在 Spring 中,Bean 的作用域决定了 Spring 容器如何创建和管理 Bean 实例。Spring 支持以下几种常用的作用域:
Singleton
描述:整个 Spring 容器中仅创建一个 Bean 实例 (默认作用域) 。
生命周期:容器启动时创建,容器关闭时销毁。
适用场景:无状态的共享组件,例如服务、DAO 等。
声明方式:默认作用域,不需要额外配置,或通过 @Scope(“singleton”) 注解显式声明。
Prototype
描述:每次请求时都会创建一个新的 Bean 实例。
生命周期:容器创建并返回 Bean 后即不再管理其生命周期。
适用场景:有状态的、频繁创建的对象,例如特定任务的 Helper 对象。
声明方式:通过 @Scope(“prototype”) 注解声明。
Request
描述:每次 HTTP 请求创建一个 Bean 实例,仅在 Web 应用上下文中有效。
生命周期:请求开始时创建,请求结束后销毁。
适用场景:需要基于请求范围的数据,例如 Web 请求中的特定处理逻辑。
声明方式:通过 @Scope(“request”) 注解声明。
Session
描述:在每个 HTTP 会话中创建一个 Bean 实例,仅在 Web 应用上下文中有效。
生命周期:会话开始时创建,会话结束后销毁。
适用场景:需要在整个会话期间维持状态的对象,例如用户会话中的数据。
声明方式:通过 @Scope(“session”) 注解声明。
Application
描述:在 ServletContext 范围内创建一个 Bean 实例,仅在 Web 应用上下文中有效。
生命周期:与 ServletContext 生命周期相同,即整个应用范围内共享。
适用场景:跨请求、跨会话的共享状态或数据。
声明方式:通过 @Scope(“application”) 注解声明。
WebSocket
描述:在 WebSocket 会话范围内创建一个 Bean 实例。
生命周期:WebSocket 会话开始时创建,会话结束后销毁。
适用场景:需要维持 WebSocket 会话状态的对象。
声明方式:通过 @Scope(“websocket”) 注解声明。
总结表
作用域 |
描述 |
生命周期 |
使用场景 |
|---|---|---|---|
Singleton |
容器中唯一实例 (默认) |
容器启动到关闭 |
无状态、共享组件 |
Prototype |
每次请求生成新实例 |
创建后不受容器管理 |
有状态、短生命周期对象 |
Request |
每个 HTTP 请求创建新实例 |
请求开始到请求结束 |
基于请求的特定数据 |
Session |
每个 HTTP 会话创建新实例 |
会话开始到会话结束 |
会话级别的用户数据 |
Application |
Web 应用范围内单实例 |
应用启动到应用关闭 |
应用级别共享数据 |
WebSocket |
每个 WebSocket 会话创建新实例 |
会话开始到会话结束 |
WebSocket 会话状态数据 |
默认情况下,Spring Bean 的作用域是 Singleton,其他作用域可根据需要通过 @Scope 注解灵活配置。
8. Spring 的 AOP 是如何实现的?
Spring 的 AOP (面向切面编程) 通过 动态代理 实现,其核心思想是将横切关注点 (如日志记录、事务管理等) 与业务逻辑分离,具体有以下几种实现方式:
JDK 动态代理
原理:使用 JDK 自带的 java.lang.reflect.Proxy 类,为实现了接口的类创建代理对象。
特点:JDK 动态代理只能代理实现了接口的类;如果目标类没有接口,则无法使用此方式。
适用场景:代理实现接口的 Bean。
CGLIB 动态代理
原理:使用 CGLIB 库 (Code Generation Library) ,生成目标类的子类来创建代理对象。
特点:适用于没有实现接口的类,CGLIB 通过字节码操作生成一个目标类的子类并拦截方法调用。
限制:如果目标类或方法是 final 的,CGLIB 无法进行代理。
适用场景:代理没有实现接口的 Bean。
AspectJ 静态编译 (编译时增强)
原理:AspectJ 是一种功能更强大的 AOP 实现,可以在编译期将切面逻辑直接编织到目标代码中。
特点:AspectJ 通过静态编译方式进行方法拦截,可以对私有方法、静态方法等执行切面增强,不受代理限制。
适用场景:需要对类进行深度增强的场景,Spring 默认不使用 AspectJ 编译增强,但可以支持 AspectJ 注解风格。
Spring AOP 的实现步骤
定义切面 (Aspect) :使用 @Aspect 注解定义切面类,通过 @Before、@After、@Around 等注解声明切入点和增强逻辑。
解析切入点表达式:Spring AOP 使用切入点表达式 (如 execution(* com.example..*(..))) 来确定哪些方法需要增强。
代理对象的创建:
Spring 首先判断目标类是否实现接口,若实现接口,则使用 JDK 动态代理;
否则,使用 CGLIB 动态代理 创建子类代理。
方法拦截与执行增强逻辑:代理对象在方法调用前后执行增强逻辑 (如前置、后置通知) ,增强的具体行为由 MethodInterceptor 等接口实现。
执行目标方法:在增强逻辑前后,最终执行目标方法,方法执行结果可以进一步处理或修改后返回给调用方。
代理方式选择
默认选择:Spring AOP 优先选择 JDK 动态代理,因为 JDK 动态代理效率较高且更轻量。
强制选择:可以通过 proxy-target-class 配置属性强制使用 CGLIB 代理,即便目标类实现了接口。
总结
Spring AOP 使用 JDK 动态代理 和 CGLIB 动态代理 实现 运行时增强,代理目标方法并应用横切逻辑。如果需要更强的编译时增强 (如对静态方法、私有方法增强) ,则可以使用 AspectJ。Spring 默认通过运行时代理实现 AOP,利用切面来增强 Bean 功能而不改变核心业务逻辑。
9. Spring 如何解决线程安全问题
在 Spring 中,线程安全性主要依赖于以下几种方式来保证:
默认的 Singleton Bean 作用域
无状态设计:Spring 中大部分 Bean 默认是单例 (singleton) 的,而单例 Bean 在多线程环境中会共享实例。因此,Spring 建议单例 Bean 保持无状态 (不存储实例变量) ,以避免多线程访问同一实例变量时发生并发问题。
状态局部化:通过局部变量 (方法内部变量) 代替实例变量,每个线程都有自己的局部变量拷贝,避免线程间数据共享。
Prototype 作用域
每次请求创建一个新实例:在需要有状态的 Bean 时,可以使用 prototype 作用域。每次请求都会创建一个新的实例,保证实例独立,适合线程不安全的组件或有状态的组件。
线程安全的依赖组件
使用线程安全的类:对于需要线程安全的组件,可以选择使用 Java 提供的线程安全类 (如 ConcurrentHashMap、AtomicInteger 等) 来管理状态。
线程安全库:如果业务中确实需要在 Singleton Bean 中存储共享状态,可以使用 Java 的并发库,如 java.util.concurrent 包中的类。
使用 @Async 异步任务执行
Spring 允许通过 @Async 注解将方法异步执行。Spring 会为异步方法生成一个独立的线程,每个任务使用独立的线程上下文,避免不同任务间的数据竞争。
配置线程池:可以配置线程池来管理 @Async 方法的执行线程数,避免线程数量过多或竞争问题。
ThreadLocal 变量
线程局部变量:可以使用 ThreadLocal 为每个线程提供一个独立的变量副本,以保证多线程访问同一个 Bean 时,线程间的变量数据隔离。例如,在拦截器或服务中使用 ThreadLocal 存储当前用户信息。
适用场景:适合线程敏感但不适合共享的数据,例如请求上下文、事务状态等。
线程安全的第三方工具
注入线程安全的 Bean:对于涉及数据库操作、缓存等需要线程安全的资源,Spring 推荐注入线程安全的第三方库 (如 DataSource、RedisTemplate) 来避免数据竞争。
事务管理
事务的隔离性:Spring 的事务管理可以确保数据库操作的原子性和隔离性,避免多线程操作相同数据造成的数据不一致问题。
注解支持:通过 @Transactional 注解,Spring 可以管理方法的事务隔离级别,确保方法执行具有一致性和数据完整性。
总结
在 Spring 中,解决线程安全问题的主要策略是 无状态设计 和 线程隔离。可以通过 prototype 作用域、ThreadLocal、异步执行、线程安全的依赖组件等方法保证多线程安全性。Spring 的单例 Bean 一般无状态,而数据库操作等依赖 Spring 事务管理来避免并发问题。