来宾市网站建设_网站建设公司_数据统计_seo优化
2025/12/20 16:24:18 网站建设 项目流程

概念

Bean 代指的就是那些被 IoC 容器所管理的对象。

下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。

将一个类声明为bean的注解

  • @Component:通用的注解,可标注任意类为Spring组件。如果一个 Bean 不知道属于哪个层,可以使用@Component注解标注。
  • @Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller: 对应 Spring MVC 控制层,主要用于接受用户请求并调用Service层返回数据给前端页面。

@Component@Bean的区别是什么?

  • @Component注解作用于,而@Bean注解作用于方法
  • @Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
  • @Bean注解比@Component注解的自定义性更强,而且很多地方我们只能通过@Bean注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring容器时,则只能通过@Bean来实现。
@Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } }

注入bean的方法

注入bean的方式有哪些?

依赖注入 (Dependency Injection, DI) 的常见方式:

  1. 构造函数注入:通过类的构造函数来注入依赖项。
  2. Setter 注入:通过类的 Setter 方法来注入依赖项。
  3. Field(字段) 注入:直接在类的字段上使用注解(如@Autowired@Resource)来注入依赖项。

Spring 官方推荐构造函数注入,这种注入方式的优势如下:

  1. 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。
  2. 不可变性:有助于创建不可变对象,提高了线程安全性。
  3. 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。
  4. 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。

@Autowired@Resource的区别是什么?

Annotation

Package

Source

@Autowired

org.springframework.bean.factory

Spring 2.5+

@Resource

javax.annotation

Java JSR-250

@Inject

javax.inject

Java JSR-330

  • @Autowired是 Spring 提供的注解,@Resource是 JDK 提供的注解。
  • @Autowired默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。@Autowired可以通过@Qualifier注解来显式指定名称,@Resource可以通过name属性来显式指定名称。
  • @Autowired支持在构造函数、方法、字段和参数上使用。@Resource主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

bean的作用域

Spring 中 Bean 的作用域通常有下面几种:

  • singleton: IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
@Service @Scope("singleton") public class UserServiceImpl implements UserService { }
  • prototype: 每次获取都会创建一个新的 bean 实例。也就是说,连续getBean()两次,得到的是不同的 Bean 实例。

补充

  • request(仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session(仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session(仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
  • websocket(仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

配置bean的作用域

<bean id="..." class="..." scope="singleton"></bean>
@Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Person personPrototype() { return new Person(); }

bean是线程安全的吗?

Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。

我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。

  • prototype作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
  • singleton作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。

什么叫做bean有状态?就是看当前的成员变量是否可以被修改,可以被修改的就是有状态bean,线程不安全,无法被修改的就是无状态bean,线程安全。

大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List @Component public class ShoppingCart { private List<String> items = new ArrayList<>(); public void addItem(String item) { items.add(item); } public List<String> getItems() { return items; } }
// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态,没有定义可变的成员变量。 @Component public class UserService { public User findUserById(Long id) { //... } //... }

对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:

  1. 避免可变成员变量: 尽量设计 Bean 为无状态。
  2. 使用ThreadLocal: 将可变成员变量保存在ThreadLocal中,确保线程独立。
  3. 使用同步机制: 利用synchronizedReentrantLock来进行同步控制,确保线程安全。
public class UserThreadLocal { private UserThreadLocal() {} private static final ThreadLocal<SysUser> LOCAL = ThreadLocal.withInitial(() -> null); public static void put(SysUser sysUser) { LOCAL.set(sysUser); } public static SysUser get() { return LOCAL.get(); } public static void remove() { LOCAL.remove(); } }

bean的生命周期

  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法,同样,如果bean使用了destroy-method声明销毁方法,该方法也会被调用。

如何记忆呢?

  1. 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
  2. 初始化这一步涉及到的步骤比较多,包含Aware接口的依赖注入、BeanPostProcessor在初始化前后的处理以及InitializingBeaninit-method的初始化操作。
  3. 销毁这一步会注册相关销毁回调接口,最后通过DisposableBeandestory-method进行销毁。

bean的循环依赖问题

循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用

eg:

CircularDependencyA → CircularDependencyB → CircularDependencyA。

@Component public class CircularDependencyA { @Autowired private CircularDependencyB circB; } @Component public class CircularDependencyB { @Autowired private CircularDependencyA circA; }

循环依赖问题在Spring中主要有三种情况:

  • 第一种:通过构造方法进行依赖注入时产生的循环依赖问题。
  • 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

只有【第三种方式】的循环依赖问题被Spring解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。

解决方案

Spring 通过 三级缓存 和 提前暴露未完全初始化的对象引用 的机制来解决单例作用域 Bean 的 setter 注入方式的循环依赖问题。

// 一级缓存 /** Cache of singleton objects: bean name to bean instance. */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存 /** Cache of early singleton objects: bean name to bean instance. */ private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 三级缓存 /** Cache of singleton factories: bean name to ObjectFactory. */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  • 一级缓存(singletonObjects):单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
  • 二级缓存(earlySingletonObjects):缓存早期的bean对象(生命周期还没走完)
  • 三级缓存(singletonFactories):缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

只用两级缓存

在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖问题。

三级缓存解决bean循环依赖

但是,当涉及到 AOP 时,三级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。

解决流程:

  • 第一步:创建 BeanA 的实例并提前暴露工厂。

Spring首先调用 BeanA 的构造函数进行实例化,此时得到一个原始对象(尚未填充属性)。紧接着,Spring会将一个特殊的 ObjectFactory 工厂对象存入第三级缓存(singletonFactories)。这个工厂的使命是:当其他Bean需要引用 BeanA 时,它能动态返回当前这个半成品的 BeanA(可能是原始对象,也可能是为应对AOP而提前生成的代理对象)。此时 BeanA 的状态是“已实例化但未初始化”,像一座刚搭好钢筋骨架的大楼。

  • 第二步:填充 BeanA 的属性时触发 BeanB 的创建。

Spring开始为 BeanA 注入属性,发现它依赖 BeanB。于是容器转向创建 BeanB,同样先调用其构造函数实例化,并将 BeanB 对应的 ObjectFactory 工厂存入三级缓存。至此,三级缓存中同时存在 BeanA 和 BeanB 的工厂,它们都代表未完成初始化的半成品。

  • 第三步:Bean 属性注入时发现循环依赖。

当Spring试图填充 Bean 的属性时,检测到它需要注入 BeanA。此时容器启动依赖查找:

    1. 在一级缓存(存放完整Bean)中未找到 BeanA;
    2. 在二级缓存(存放已暴露的早期引用)中同样未命中;
    3. 最终在三级缓存中定位到 BeanA 的工厂。

Spring立即调用该工厂的 getObject()方法。这个方法会执行关键决策:若 BeanA 需要AOP代理,则动态生成代理对象(即使 BeanA 还未初始化);若无需代理,则直接返回原始对象。得到的这个早期引用(可能是代理)被放入二级缓存(earlySingletonObjects),同时从三级缓存清理工厂条目。最后,Spring将这个早期引用注入到 BeanB 的属性中。至此,BeanB 成功持有 BeanA 的引用——尽管 BeanA 此时仍是个半成品。

  • 第四步:完成 BeanB 的生命周期。

BeanB 获得所有依赖后,Spring执行其初始化方法(如@postConstruct),将其转化为完整可用的 Bean。随后,BeanB 被提升至一级缓存(singletonObjects),二级和三级缓存中关于 BeanB 的临时条目均被清除。此时 BeanB 已准备就绪,可被其他对象使用。

  • 第五步:回溯完成 BeanA 的构建。

随着 BeanB 创建完毕,流程回溯到最初中断的 BeanA 属性注入环节。Spring将已完备的 BeanB 实例注入 BeanA,接着执行 BeanA 的初始化方法。这里有个精妙细节:若之前为 BeanA 生成过早期代理,Spring会直接复用二级缓存中的代理对象作为最终Bean,而非重复创建。最终,完全初始化的 BeanA(可能是原始对象或代理)入驻一级缓存,其早期引用从二级缓存移除。至此循环闭环完成,两个Bean皆可用。

@Lazy能解决循环依赖吗?

@Lazy用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。

Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。

#默认false spring.main.lazy-initialization=true
SpringApplication springApplication=new SpringApplication(Start.class); springApplication.setLazyInitialization(false); springApplication.run(args);

@Lazy如何解决循环依赖问题

eg:

比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加@Lazy注解之后(延迟 Bean B 的实例化),加载的流程如下:

  • 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
  • 由于在 A 上标注了@Lazy注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
  • 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。

从上面的加载流程可以看出:@Lazy解决循环依赖的关键点在于代理对象的使用。

  • 没有@Lazy的情况下:在 Spring 容器初始化A时会立即尝试创建B,而在创建B的过程中又会尝试创建A,最终导致循环依赖(即无限递归,最终抛出异常)。
  • 使用@Lazy的情况下:Spring 不会立即创建B,而是会注入一个B的代理对象。由于此时B仍未被真正初始化,A的初始化可以顺利完成。等到A实例实际调用B的方法时,代理对象才会触发B的真正初始化。

@Lazy能够在一定程度上打破循环依赖链,允许 Spring 容器顺利地完成 Bean 的创建和注入。但这并不是一个根本性的解决方案,尤其是在构造函数注入、复杂的多级依赖等场景中,@Lazy无法有效地解决问题。因此,最佳实践仍然是尽量避免设计上的循环依赖。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询