Spring FAQ

1. Spring 如何解决循环依赖的问题?

三级缓存, 并提前暴露 Bean

在 Spring 中,循环依赖是指两个或多个 bean 相互依赖,形成依赖闭环。例如,BeanA 依赖 BeanB,而 BeanB 又依赖 BeanA。Spring 通过三级缓存机制 (三级缓存) 来解决这种循环依赖问题。

Spring 的三级缓存机制

Spring 使用三级缓存机制来解决循环依赖。三级缓存分为以下三个层次:

  1. 一级缓存 (singletonObjects) :用于存放完全初始化好的单例 bean。这个缓存中存储的是已经完成初始化的 bean,之后直接从这里获取 bean 实例。

  2. 二级缓存 (earlySingletonObjects) :用于存放早期暴露的单例对象,通常是已经实例化但未完成属性注入和初始化的 bean 实例。这一缓存中的 bean 是可以被代理的,以便在循环依赖中注入代理对象。

  3. 三级缓存 (singletonFactories) :用于存放可以创建早期 bean 的工厂对象。这个缓存中的对象是 ObjectFactory 类型的 lambda 表达式或方法引用,它们负责生成 bean 实例,并将其暴露给二级缓存。

三级缓存的工作流程

假设 BeanA 和 BeanB 存在循环依赖,Spring 解决该问题的流程如下:

  1. 实例化:Spring 在创建 BeanA 时,首先会在 singletonObjects (一级缓存) 中查找 BeanA。如果找不到,Spring 会先实例化 BeanA,但不会立即填充它的依赖属性。

  2. 暴露早期对象:Spring 将 BeanA 的 ObjectFactory 放入三级缓存中 (singletonFactories) 。ObjectFactory 是一个 lambda 表达式或方法引用,用于生成 BeanA 的代理对象。此时,BeanA 尚未完成初始化,但可以提前暴露其引用。

  3. 创建依赖的 BeanB:在填充 BeanA 的依赖属性时,发现 BeanA 依赖 BeanB。因此,Spring 开始创建 BeanB,并执行同样的步骤,将 BeanB 的 ObjectFactory 暴露到三级缓存中。

  4. 检测循环依赖:在创建 BeanB 时,发现 BeanB 依赖 BeanA。Spring 在二级缓存 (earlySingletonObjects) 和三级缓存中查找 BeanA。此时,BeanA 的 ObjectFactory 已经在三级缓存中存在,因此 Spring 调用 ObjectFactory 获取 BeanA 的代理对象,并将该代理对象放入二级缓存 (earlySingletonObjects) 。

  5. 完成依赖注入:BeanB 获取到 BeanA 的早期对象 (代理对象) ,并成功完成初始化。然后,Spring 将 BeanB 从三级缓存转移到一级缓存 (singletonObjects) 中。

  6. 完成 BeanA 的初始化:回到 BeanA 的初始化,BeanA 的依赖 (BeanB) 已经完成注入,Spring 继续完成 BeanA 的初始化,并将 BeanA 放入一级缓存中。

通过上述流程,Spring 成功解决了 BeanA 和 BeanB 的循环依赖。

需要注意的几点

  1. 三级缓存的使用:三级缓存的引入是为了支持代理机制,即提前暴露不完整的 bean。二级缓存存放的是已经实例化但尚未完全初始化的对象。三级缓存则用于存放对象工厂,以便在需要时创建早期代理对象。

  2. 构造器注入无法解决循环依赖:Spring 的三级缓存机制只能解决属性注入的循环依赖。对于构造器注入的循环依赖,Spring 无法解决,因为构造器注入需要在实例化时完全提供依赖对象。

  3. 可以通过 @Lazy 注解延迟加载:通过将依赖标记为 @Lazy,Spring 可以推迟实例化依赖的对象,从而避免循环依赖问题。

  4. 代理对象在循环依赖中的作用:在一些场景中,Spring 会使用 CGLIB 动态代理技术生成代理对象,这样可以提前暴露不完整的 bean,以便完成依赖注入。

2. Spring Boot 的自动装配过程是如何实现的?

spring boot 启动时会通过 @EnableAutoConfiguration 找到 META-INF/spring.factories 文件中的自动配置类 AutoConfiguration,然后根据条件加载这些类,从而完成自动配置。

  1. 启动类和 @SpringBootApplication

  • @SpringBootApplication 是复合注解,包含 @EnableAutoConfiguration、@ComponentScan 和 @Configuration。

  • @EnableAutoConfiguration 启用自动配置,扫描 META-INF/spring.factories 文件中的自动配置类。

  1. @EnableAutoConfiguration 和 spring.factories

  • @EnableAutoConfiguration 配合 SpringFactoriesLoader 读取 spring-boot-autoconfigure JAR 包中的 META-INF/spring.factories 文件。

  • spring.factories 列出了所有自动配置类,它们在 Spring 容器启动时被逐个加载并处理。

  1. 条件注解 @Conditional

  • 自动配置类通常使用 @Conditional 系列注解 (如 @ConditionalOnMissingBean、@ConditionalOnClass) 来控制配置的生效。

  • 这些条件注解确保在满足特定条件 (如类存在、Bean 不存在) 时才加载相应的 Bean。

  1. 自动配置类的加载与执行

  • Spring Boot 将所有符合条件的自动配置类加载到应用上下文中。

  • 自动配置类定义了许多 @Bean 方法来创建和配置组件,这些 Bean 会自动注入到 Spring 应用上下文中。

  1. 自定义配置与优先级

  • 用户自定义的配置 (例如 application.properties) 会覆盖默认的自动配置。

  • 自定义 @Configuration 类或 @Bean 方法会优先于自动配置类中的默认 Bean 定义。

  1. 总结

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 容器管理,实现对象之间的松耦合。其核心流程如下:

  1. Bean 定义与扫描

  • Bean 定义:Spring 使用 XML 文件或注解 (如 @Component、@Service、@Controller) 定义 Bean。

  • 扫描与注册:Spring 扫描应用中的配置类或指定包,将符合条件的类注册为 Bean。

  1. BeanFactory 和 ApplicationContext

  • BeanFactory:提供最基本的 IoC 容器功能,通过懒加载管理 Bean 的创建和依赖注入。

  • ApplicationContext:扩展自 BeanFactory,提供更丰富的功能,如事件发布、国际化支持和自动扫描。

  1. Bean 的生命周期

  • 实例化:Spring 使用反射实例化 Bean。

  • 依赖注入:Spring 通过构造器、Setter 或字段注入,将所需依赖注入到 Bean 中。

  • 初始化:调用 @PostConstruct、InitializingBean 等方法,完成 Bean 的初始化。

  • 销毁:在容器关闭时调用 @PreDestroy、DisposableBean 等方法销毁 Bean。

  1. 依赖注入 (DI)

  • 构造器注入:通过构造函数完成依赖注入,适合不可变依赖。

  • Setter 注入:通过 Setter 方法完成依赖注入,适合可选依赖。

  • 字段注入:通过 @Autowired 等注解直接注入字段,简化依赖管理。

  1. 代理与 AOP 支持

  • Spring 使用动态代理生成 Bean 的代理对象,支持 AOP 功能 (如事务、日志等) ,进一步增强 Bean 的功能。

总结

Spring IoC 容器通过依赖注入管理 Bean 的生命周期和依赖关系,简化了对象间的协作和配置,实现了松耦合、可扩展的应用架构。

5. Spring Bean 的生命周期是怎样的?

Spring Bean 的生命周期包含实例化、依赖注入、初始化、销毁等阶段,可以通过实现接口 Lifecycle 或 BeanPostProcessor 等方式来定制生命周期。

Spring Bean 的生命周期是指从实例化到销毁的整个过程,主要分为以下几个阶段:

  1. 实例化 (Instantiation)

  • Spring 容器根据 Bean 定义使用反射机制创建 Bean 实例,但此时还未初始化依赖。

  1. 属性注入 (Populate Properties)

  • Spring 为 Bean 注入依赖 (DI) ,完成属性值的设置。这可以通过构造器、Setter 方法或直接字段注入实现。

  1. 初始化 (Initialization)

  • BeanNameAware、BeanFactoryAware 等接口:如果 Bean 实现了这些接口,Spring 会调用相应的回调方法,将容器相关信息传递给 Bean。

  • BeanPostProcessor 前置处理:在 Bean 初始化方法前,Spring 会调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法。

  • 初始化方法:

    • 如果实现了 InitializingBean 接口,会调用 afterPropertiesSet 方法。

    • 如果定义了 init-method 或使用了 @PostConstruct 注解,则会执行对应的初始化方法。

  • BeanPostProcessor 后置处理:在 Bean 初始化后,Spring 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法,可能返回增强后的 Bean (如 AOP 代理) 。

  1. 使用阶段

  • Bean 完成初始化后,处于就绪状态,可以在应用中被使用。它在 Spring 容器中存续,直到容器销毁。

  1. 销毁 (Destruction)

  • 当容器关闭时,Spring 会对单例作用域的 Bean 执行销毁操作。

  • 销毁方法:

    • 如果实现了 DisposableBean 接口,会调用其 destroy 方法。

    • 如果定义了 destroy-method 或使用了 @PreDestroy 注解,则会执行相应销毁方法。

生命周期流程总结图

实例化 -> 属性注入 -> 初始化前处理 -> 初始化 -> 初始化后处理 -> 使用 -> 销毁
  • 总结

Spring Bean 的生命周期由容器管理,通过回调接口和注解实现定制化的初始化和销毁操作,使 Bean 的管理更灵活和可扩展。

6. Spring 依赖注入有哪几种方式

注入方式

优点

缺点

适用场景

构造器注入

强制依赖完整性,适合不可变依赖

代码冗长

必须的依赖

Setter 注入

灵活,适合可选依赖

依赖完整性不保证

可选的、可变的依赖

字段注入

简洁,代码量少

不利于测试和解耦

简单场景

通常推荐优先使用 构造器注入,其次是 Setter 注入,字段注入则仅适用于简单场景。

7. Spring Bean 的作用域有哪些

在 Spring 中,Bean 的作用域决定了 Spring 容器如何创建和管理 Bean 实例。Spring 支持以下几种常用的作用域:

  1. Singleton

  • 描述:整个 Spring 容器中仅创建一个 Bean 实例 (默认作用域) 。

  • 生命周期:容器启动时创建,容器关闭时销毁。

  • 适用场景:无状态的共享组件,例如服务、DAO 等。

  • 声明方式:默认作用域,不需要额外配置,或通过 @Scope(“singleton”) 注解显式声明。

  1. Prototype

  • 描述:每次请求时都会创建一个新的 Bean 实例。

  • 生命周期:容器创建并返回 Bean 后即不再管理其生命周期。

  • 适用场景:有状态的、频繁创建的对象,例如特定任务的 Helper 对象。

  • 声明方式:通过 @Scope(“prototype”) 注解声明。

  1. Request

  • 描述:每次 HTTP 请求创建一个 Bean 实例,仅在 Web 应用上下文中有效。

  • 生命周期:请求开始时创建,请求结束后销毁。

  • 适用场景:需要基于请求范围的数据,例如 Web 请求中的特定处理逻辑。

  • 声明方式:通过 @Scope(“request”) 注解声明。

  1. Session

  • 描述:在每个 HTTP 会话中创建一个 Bean 实例,仅在 Web 应用上下文中有效。

  • 生命周期:会话开始时创建,会话结束后销毁。

  • 适用场景:需要在整个会话期间维持状态的对象,例如用户会话中的数据。

  • 声明方式:通过 @Scope(“session”) 注解声明。

  1. Application

  • 描述:在 ServletContext 范围内创建一个 Bean 实例,仅在 Web 应用上下文中有效。

  • 生命周期:与 ServletContext 生命周期相同,即整个应用范围内共享。

  • 适用场景:跨请求、跨会话的共享状态或数据。

  • 声明方式:通过 @Scope(“application”) 注解声明。

  1. 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 (面向切面编程) 通过 动态代理 实现,其核心思想是将横切关注点 (如日志记录、事务管理等) 与业务逻辑分离,具体有以下几种实现方式:

  1. JDK 动态代理

    • 原理:使用 JDK 自带的 java.lang.reflect.Proxy 类,为实现了接口的类创建代理对象。

    • 特点:JDK 动态代理只能代理实现了接口的类;如果目标类没有接口,则无法使用此方式。

    • 适用场景:代理实现接口的 Bean。

  2. CGLIB 动态代理

    • 原理:使用 CGLIB 库 (Code Generation Library) ,生成目标类的子类来创建代理对象。

    • 特点:适用于没有实现接口的类,CGLIB 通过字节码操作生成一个目标类的子类并拦截方法调用。

    • 限制:如果目标类或方法是 final 的,CGLIB 无法进行代理。

    • 适用场景:代理没有实现接口的 Bean。

  3. AspectJ 静态编译 (编译时增强)

    • 原理:AspectJ 是一种功能更强大的 AOP 实现,可以在编译期将切面逻辑直接编织到目标代码中。

    • 特点:AspectJ 通过静态编译方式进行方法拦截,可以对私有方法、静态方法等执行切面增强,不受代理限制。

    • 适用场景:需要对类进行深度增强的场景,Spring 默认不使用 AspectJ 编译增强,但可以支持 AspectJ 注解风格。

Spring AOP 的实现步骤

  1. 定义切面 (Aspect) :使用 @Aspect 注解定义切面类,通过 @Before、@After、@Around 等注解声明切入点和增强逻辑。

  2. 解析切入点表达式:Spring AOP 使用切入点表达式 (如 execution(* com.example..*(..))) 来确定哪些方法需要增强。

  3. 代理对象的创建:

    • Spring 首先判断目标类是否实现接口,若实现接口,则使用 JDK 动态代理;

    • 否则,使用 CGLIB 动态代理 创建子类代理。

  4. 方法拦截与执行增强逻辑:代理对象在方法调用前后执行增强逻辑 (如前置、后置通知) ,增强的具体行为由 MethodInterceptor 等接口实现。

  5. 执行目标方法:在增强逻辑前后,最终执行目标方法,方法执行结果可以进一步处理或修改后返回给调用方。

代理方式选择

  • 默认选择:Spring AOP 优先选择 JDK 动态代理,因为 JDK 动态代理效率较高且更轻量。

  • 强制选择:可以通过 proxy-target-class 配置属性强制使用 CGLIB 代理,即便目标类实现了接口。

总结

Spring AOP 使用 JDK 动态代理 和 CGLIB 动态代理 实现 运行时增强,代理目标方法并应用横切逻辑。如果需要更强的编译时增强 (如对静态方法、私有方法增强) ,则可以使用 AspectJ。Spring 默认通过运行时代理实现 AOP,利用切面来增强 Bean 功能而不改变核心业务逻辑。

9. Spring 如何解决线程安全问题

在 Spring 中,线程安全性主要依赖于以下几种方式来保证:

  1. 默认的 Singleton Bean 作用域

  • 无状态设计:Spring 中大部分 Bean 默认是单例 (singleton) 的,而单例 Bean 在多线程环境中会共享实例。因此,Spring 建议单例 Bean 保持无状态 (不存储实例变量) ,以避免多线程访问同一实例变量时发生并发问题。

  • 状态局部化:通过局部变量 (方法内部变量) 代替实例变量,每个线程都有自己的局部变量拷贝,避免线程间数据共享。

  1. Prototype 作用域

  • 每次请求创建一个新实例:在需要有状态的 Bean 时,可以使用 prototype 作用域。每次请求都会创建一个新的实例,保证实例独立,适合线程不安全的组件或有状态的组件。

  1. 线程安全的依赖组件

  • 使用线程安全的类:对于需要线程安全的组件,可以选择使用 Java 提供的线程安全类 (如 ConcurrentHashMap、AtomicInteger 等) 来管理状态。

  • 线程安全库:如果业务中确实需要在 Singleton Bean 中存储共享状态,可以使用 Java 的并发库,如 java.util.concurrent 包中的类。

  1. 使用 @Async 异步任务执行

  • Spring 允许通过 @Async 注解将方法异步执行。Spring 会为异步方法生成一个独立的线程,每个任务使用独立的线程上下文,避免不同任务间的数据竞争。

  • 配置线程池:可以配置线程池来管理 @Async 方法的执行线程数,避免线程数量过多或竞争问题。

  1. ThreadLocal 变量

  • 线程局部变量:可以使用 ThreadLocal 为每个线程提供一个独立的变量副本,以保证多线程访问同一个 Bean 时,线程间的变量数据隔离。例如,在拦截器或服务中使用 ThreadLocal 存储当前用户信息。

  • 适用场景:适合线程敏感但不适合共享的数据,例如请求上下文、事务状态等。

  1. 线程安全的第三方工具

  • 注入线程安全的 Bean:对于涉及数据库操作、缓存等需要线程安全的资源,Spring 推荐注入线程安全的第三方库 (如 DataSource、RedisTemplate) 来避免数据竞争。

  1. 事务管理

  • 事务的隔离性:Spring 的事务管理可以确保数据库操作的原子性和隔离性,避免多线程操作相同数据造成的数据不一致问题。

  • 注解支持:通过 @Transactional 注解,Spring 可以管理方法的事务隔离级别,确保方法执行具有一致性和数据完整性。

总结

在 Spring 中,解决线程安全问题的主要策略是 无状态设计 和 线程隔离。可以通过 prototype 作用域、ThreadLocal、异步执行、线程安全的依赖组件等方法保证多线程安全性。Spring 的单例 Bean 一般无状态,而数据库操作等依赖 Spring 事务管理来避免并发问题。

过滤器与拦截器有什么区别