Spring Bean

@[toc] --- ## 什么是 Spring Bean? 交给 Spring 管理的对象也称为 `Bean`,所有的 Bean 都被存储在 IoC 容器中 我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/062b422bd7ac4d53afd28fb74b2bc94d.png) ## 创建 Spring Bean 的方法有哪些 ### 使用构造函数创建 Bean 可以通过在配置类中使用 `@Bean` 注解,并提供相应的构造函数参数,来创建 Bean 实例。示例代码如下: ```java @Configuration public class MyConfiguration { @Bean public MyBean myBean() { return new MyBean(); // 使用默认的无参构造函数创建 Bean 实例 } @Bean public AnotherBean anotherBean() { return new AnotherBean(myBean()); // 通过构造函数注入依赖的 Bean } } ``` ### 使用静态工厂方法创建 Bean 可以通过在配置类中使用 `@Bean` 注解,并提供一个工厂方法(静态工厂方法)来创建 Bean 实例。示例代码如下: ```java @Configuration public class MyConfiguration { @Bean public MyBean myBean() { return MyBeanFactory.createMyBean(); // 调用工厂方法创建 Bean 实例 } } public class MyBeanFactory { public static MyBean createMyBean() { return new MyBean(); // 工厂方法创建 Bean 实例 } } ``` ### 使用实例工厂方法创建 Bean 如果 Bean 的创建依赖于其他 Bean 实例,可以使用实例工厂方法创建 Bean。示例代码如下: ```java @Configuration public class MyConfiguration { @Bean public MyBeanFactory myBeanFactory() { return new MyBeanFactory(); // 实例工厂方法 } @Bean public MyBean myBean() { return myBeanFactory().createMyBean(); // 调用实例工厂方法创建 Bean 实例 } } public class MyBeanFactory { public MyBean createMyBean() { return new MyBean(); // 实例工厂方法创建 Bean 实例 } } ``` ### 使用注解方式创建 Bean 可以使用 `@Component` 注解或其衍生注解(`@Service`、`@Repository`、`@Controller`)标记类,并让 Spring 自动扫描并创建 Bean 实例: - `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 - `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面 这些注解的作用完全一样,只不过方便标注在不同层 示例代码如下: ```java @Component public class MyBean { // Bean 的实现 } ``` 在 Spring 的配置文件中,需要配置组件扫描的基础包,以便 Spring 能够扫描并创建被标记的类。 ## @Component 和 @Bean 的区别是什么? - **`@Component` 注解作用于类,而 `@Bean` 注解作用于方法** - `@Component`通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。 `@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 Bean,`@Bean` 告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。 - **`@Bean` 注解比 `@Component` 注解的自定义性更强**,而且很多地方我们只能通过 `@Bean` 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean` 来实现。 `@Bean` 注解使用示例: ```java @Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } ``` 上面的代码相当于下面的 xml 配置 ```xml <beans> <bean id="transferService" class="com.acme.TransferServiceImpl"/> </beans> ``` 下面这个例子是通过 `@Component` 无法实现的。 ```java @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1(); when 2: return new serviceImpl2(); when 3: return new serviceImpl3(); } } ``` ## 注入 Bean 的注解有哪些? Spring 内置的 `@Autowired` 以及 JDK 内置的 `@Resource` 和 `@Inject` 都可以用于注入 Bean。 | Annotaion | Package | Source | | ------------ | ---------------------------------- | ------------ | | `@Autowired` | `org.springframework.bean.factory` | Spring 2.5+ | | `@Resource` | `javax.annotation` | Java JSR-250 | | `@Inject` | `javax.inject` | Java JSR-330 | `@Autowired` 和 `@Resource` 使用的比较多一些 ## @Autowired 和 @Resource 的区别是什么? `Autowired` 属于 Spring 内置的注解,**默认的注入方式为 `byType`(根据属性类型进行匹配)**,也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。 当一个接口存在多个实现类的话,`byType` 这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。**这种情况下,注入方式会变为 `byName`(根据属性名称进行匹配)**,这个名称通常就是类名(首字母小写)。就比如说下面代码中的 `smsService` 就是我这里所说的名称 ```java // smsService 就是我们上面所说的名称 @Autowired private SmsService smsService; ``` > 举个例子,`SmsService` 接口有两个实现类: `SmsServiceImpl1`和 `SmsServiceImpl2`,且它们都已经被 Spring 容器所管理。 > > ```java > // 报错,byType 有多个且 byName 无法匹配 > @Autowired > private SmsService smsService; > > // 正确注入 SmsServiceImpl1 > @Autowired > private SmsService smsServiceImpl1; > > // 正确注入 SmsServiceImpl1 > // 通过 Qualifier 指定 name = "smsServiceImpl1" > @Autowired > @Qualifier(value = "smsServiceImpl1") > private SmsService smsService; > ``` > > 我们还是建议通过 `@Qualifier` 注解来显示指定名称而不是依赖变量的名称。 `@Resource` 属于 JDK 提供的注解,和 @Autowired 恰恰相反,它的**默认注入方式为 `byName`。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为 `byType`**。 `@Resource` 有两个比较重要且日常开发常用的属性:`name`(名称)、`type`(类型)。 ```java public @interface Resource { String name() default ""; Class<?> type() default Object.class; } ``` 如果仅指定 `name` 属性则注入方式为 `byName`,如果仅指定 `type` 属性则注入方式为 `byType`,如果同时指定`name` 和`type`属性(不建议这么做)则注入方式为 `byType` + `byName`。 > 举个例子,`SmsService` 接口有两个实现类: `SmsServiceImpl1`和 `SmsServiceImpl2`,且它们都已经被 Spring 容器所管理。 > > ```java > // 报错,byName 和 byType 都无法匹配到 bean > @Resource > private SmsService smsService; > > // 正确注入 SmsServiceImpl1 > @Resource > private SmsService smsServiceImpl1; > > // 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式) > @Resource(name = "smsServiceImpl1") > private SmsService smsService; > ``` 简单总结一下: - `@Autowired` 是 Spring 提供的注解,`@Resource` 是 JDK 提供的注解。 - `Autowired` 默认的注入方式为 `byType`(根据属性类型进行匹配),如果查不到则再根据属性名称进行匹配 `byName` `@Resource` 默认注入方式为 `byName`,如果查不到则再根据属性类型进行匹配 `byType` - 当一个接口存在多个实现类的情况下,`@Autowired` 和 `@Resource` 都需要通过名称才能正确匹配到对应的 Bean。`Autowired` 可以通过 `@Qualifier` 注解来显示指定名称,`@Resource` 可以直接通过内置的 `name` 属性来显示指定名称 ## Bean 作用域 在定义 Bean 时,用户不但可以配置 Bean 的属性值及相互之间的依赖关系,还可以定义 Bean 的作用域。**作用域将对 Bean 的生命周期和创建方式产生影响。** > **如何配置 bean 的作用域呢?** > > xml 方式: > > ```xml > <bean id="..." class="..." scope="singleton"></bean> > ``` > > 注解方式: > > ```java > @Bean > @Scope("singleton") > public Person personPrototype() { > return new Person(); > } > ``` Bean 的作用域如下 ⬇️ ### singleton(默认) singleton 的 Bean 在同一 Spring 容器中只有一个实例,Spring 中的 bean 默认都是单例的,属于单例设计模式的应用 > 举个例子: > > ```xml > <bean id="car" class="com.smart.scope.Car" scope="singleton"/> 1 > <bean id="bossl" class="com.smart.scope.Boss" p: car-ref="car"/> 2 > <bean id="boss2" class="com.smart.scope.Boss" p: car-ref="car"/> 3 > <bean id="boss3" class="com.smart.scope.Boss" p: car-ref="car"/> 4 > ``` > > 1 处的 car Bean 声明为 singleton(**默认是 singleton,所以其实无须显式指定**),在容器中有 3 个其他的 Bean 引用了 car Bean,如 2、3、4 所示。在容器内部,boss1、boss2 和 boss3 的 car 属性都指向同一个 Bean > > <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220708152040737.png" style="zoom:67%;" /> 需要注意的是,**只有作用域为 Singleton 的 Bean 由 Spring 容器负责管理和维护,而其他作用域的 Bean 需要在适当的时候手动管理其创建和销毁**。 ### prototype 每次通过容器的 getBean 方法返回的都是一个新的 bean 实例。在默认情况下,Spring 容器在启动时不实例化 prototype 的 Bean。此外,Spring 容器将 prototype的 Bean 交给调用者后,就不再管理它的生命周期 > 举个例子: > > ```xml > <bean id="car" class="com.smart.scope.Car" scope="prototype"/> 1 > <bean id="bossl" class="com.smart.scope.Boss" p: car-ref="car"/> 2 > <bean id="boss2" class="com.smart.scope.Boss" p: car-ref="car"/> 3 > <bean id="boss3" class="com.smart.scope.Boss" p: car-ref="car"/> 4 > ``` > > <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220708152149157.png" style="zoom:50%;" /> ### request 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 ### session 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 假设我们有一个用户登录的功能,需要在用户的整个会话期间保持用户的登录状态。 在这种情况下,可以将用户对象的作用域设置为 Session,以便在同一个用户的不同请求之间共享同一个用户对象。这样可以确保用户的登录状态在整个会话期间保持一致。 ```java @Component @Scope("session") public class User { // ... } ``` ### global-session 全局 session 作用域,仅仅在基于 portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。 ## 单例 Bean 的线程安全问题及解决方案 ### 单例 Bean 的线程安全问题 先来解释下线程安全和**无状态**、**有状态**: - 所谓无状态就是不会保存数据(不存在写数据,当然也就不存在线程安全问题了) - 而有状态就是有数据存储功能(就是除了读数据它还写数据了),可以这样讲,线程安全问题都是由全局变量及静态变量引起的。换句话说,**若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的**),比如 controller 中存着一个可变的全局成员变量,如下例: ```java @RestController public class TestController { // 一个非线程安全变量(全局变量) private int var = 0; @GetMapping(value = "/test_var") public String test() { var ++; System.out.println("普通变量 var:" + var); return "普通变量 var:" + var ; } } ``` 当多个用户同时请求一个服务器时,tomcat 会给每一个请求分配一个线程,这时多个线程会并发执行该请求所对应的业务逻辑(controller里的方法) 在 postman 里面发三次请求,结果如下: ``` 普通变量 var:1 普通变量 var:2 普通变量 var:3 ``` 说明他不是线程安全的。 而 Controller、Service 和 Dao 层本身因为可以**携带状态**所以并不是线程安全的!!!只不过我们实际开发中很少去定义这种非线程安全的变量罢了(也就是我们常说的大部分 Bean 其实都是无状态的,所以在某种程度上来说 Bean 其实是安全的)。 Spring 中的 Bean 默认是单例模式的,框架并没有对 Bean 进行多线程的封装处理,那么,如果遇到这种情况,该怎么办呢? ### 解决方案 1)**更换 Bean 的作用域为原型(多例)模式 `@scope("prototype")`**,这样每次请求 Bean 就相当于是 new Bean() 这样就可以保证线程的安全了 > 还是上面那个例子: > > ```java > @RestController > @Scope(value = "prototype") > public class TestController { > > // 一个非线程安全变量(全局变量) > private int var = 0; > > @GetMapping(value = "/test_var") > public String test() { > var ++; > System.out.println("普通变量 var:" + var); > return "普通变量 var:" + var ; > } > } > ``` > > 这样一来,每个请求都单独创建一个Controller容器,所以各个请求之间是线程安全的,三次请求结果: > > ``` > 普通变量 var:1 > 普通变量 var:1 > 普通变量 var:1 > ``` > > 那么,多实例 Bean 是不是一定就线程安全呢? > > ```java > @RestController > @Scope(value = "prototype") > public class TestController { > private int var = 0; > private static int staticVar = 0; > > @GetMapping(value = "/test_var") > public String test() { > var ++; > staticVar ++; > System.out.println("普通变量 var:" + var + ",静态变量 staticVar:" + staticVar); > return "普通变量 var:" + var + "静态变量 staticVar:" + staticVar; > } > } > ``` > > 看三次请求结果: > > ``` > 普通变量 var:1,静态变量 staticVar:1 > 普通变量 var:1,静态变量 staticVar:2 > 普通变量 var:1,静态变量 staticVar:3 > ``` > > **静态变量始终是只有一份的,即便是多例 Bean,也是非线程安全的** > > **所以,尽量不要在 Controller/Service 等容器中定义静态变量,不论是单例还是多例,他都是线程不安全的** 2)**使用 ThreadLocal** 可以将一些非线程安全的变量以 ThreadLocal 存放,在同一次请求响应的调用线程中,所有对象所访问的同一 ThreadLocal 变量都是当前线程所绑定的。 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220708153912436.png" style="zoom: 67%;" /> > Spring 中事务底层 `TransactionSynchronizationManager` 就大量使用了 ThreadLocal > > ```java > @RestController > public class TestController { > private int var = 0; // 定义一个普通变量 > ThreadLocal<Integer> var = new ThreadLocal<>(); // 用 ThreadLocal 来封装变量 > ``` ## Bean 的生命周期 对于 Spring Bean 的生命周期来说,只有四个步骤: - 实例化 Instantiation - 属性赋值 Populate - 初始化 Initialization - 销毁 Destruction 整个生命周期相关的方法可以分类为以下三种: - **Bean 自身的方法**:对应着上文说的实例化 -> 属性赋值 -> 初始化 -> 销毁四个阶段。 - **容器级生命周期方法**:主要是后处理器方法,比如 `InstantiationAwareBeanPostProcessor`、`BeanPostProcessor` 接口方法。这些接口的实现类是独立于 Bean 的,并且会注册到 Spring 容器中。在 Spring 容器创建任何 Bean 的时候,这些后处理器都会发生作用。 - **Bean 级生命周期方法**:可以理解为 Bean 类直接实现接口的方法,比如 `BeanNameAware`、`BeanFactoryAware`、`ApplicationContextAware`、`InitializingBean`、`DisposableBean` 等方法,这些方法只对当前 Bean 生效 具体过程如下面两张图所示,配合文字食用(下图中的 postProcessProperties 改为 `postProcessPropertyValues`): ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220721190638078.png) 1. 当调用者通过 `getBean(beanName)` 向容器请求某一个 Bean 时,如果容器注册了 `InstantiationAwareBeanPostProcessor` 接口,则在实例化 Bean(调用 Bean 的构造函数) 之前,将调用接口的 `postProcessBeforeInstantiation()` 方法 2. ⭐ **实例化 Instantiation**:根据配置情况调用 Bean 构造函数或工厂方法实例化 Bean 3. 如果容器注册了 `InstantiationAwareBeanPostProcessor` 接口,那么在实例化Bean之后,调用该接口的 `postProcessAfterInstantiation()` 方法,可在这里对已经实例化的对象进行一些“梳妆打扮” 4. 随后,在设置每个属性之前将先调用 `InstantiationAwareBeanPostProcessor` 接口的 `postProcessPropertyValues()` 方法 5. ⭐ **属性赋值 Populate**:调用 Bean 的 set 方法【设置属性值】 6. 如果 Bean 实现了 `BeanNameAware` 接口,则将调用 setBeanName() 接口方法,将配置文件中该 Bean 对应的名称设置到 Bean 中 7. 如果 Bean 实现了 `BeanClassLoaderAware` 接口,则将调用 setClassLoader() 接口方法,将类加载器设置到 Bean 中 8. 如果 Bean 实现了 `BeanFactoryAware` 接口,则将调用 setBeanFactory() 接口方法,将 BeanFactory 容器实例设置到 Bean 中 9. 如果 BeanFactory 装配了 `BeanPostProcessor` 后处理器,则将调用 `BeanPostProcessor` 的 `Object postProcessBeforeInitialization(Object bean, String beanName)` 接口方法对 Bean 进行加工操作。 其中,入参 bean 是当前正在处理的 Bean,而 beanName 是当前 Bean 的配置名,返回的对象为加工处理后的 Bean。用户可以使用该方法对某些 Bean 进行特殊的处理,甚至改变 Bean 的行为。 > BeanPostProcessor 在 Spring 框架中占有重要的地位,为容器提供对 Bean 进行后续加工处理的切入点,Spring 容器所提供的各种 “神奇功能”(如 AOP、动态代理等)都通过 BeanPostProcessor 实施。 10. 如果 Bean 实现了 `InitializingBean` 接口,则将调用接口的 `afterPropertiesSet()` 方法 11. ⭐ **初始化 Initialization**:如果在 `<bean>` 中通过 `init-method` 属性定义了初始化方法,则将执行这个方法 12. `BeanPostProcessor` 后处理器定义了两个方法:其一是 postProcessBeforeInitialization(),在第(9)步调用;其二是 `Object postProcessAfterInitialization(Object bean,String beanName)`,这个方法在此时调用,容器再次获得对 Bean 进行加工处理的机会 13. 如果在 `<bean>` 中指定 Bean 的作用范围为 `scope="prototype"`,则将 Bean 返回给调用者,调用者负责 Bean 后续生命的管理,Spring 不再管理这个 Bean的 生命周期。 如果将作用范围设置为 `scope="singleton"`,则将 Bean 放入 Spring IoC 容器的缓存池中,并将 Bean 引用返回给调用者,Spring 继续对这些 Bean 进行后续的生命管理 14. ⭐ **销毁 Destruction**:对于 scope="singleton" 的 Bean(默认情况),当容器关闭时,将触发 Spring 对 Bean 后续生命周期的管理工作。如果 Bean 实现了 `DisposableBean` 接口,则将调用接口的 destory() 方法,可以在此编写释放资源、记录日志等操作;随后,如果通过 `<bean>` 的 `destroy-method` 属性指定了 Bean 的销毁方法,那么 Spring 将执行 Bean 的这个方法,完成 Bean 资源的释放等操作 再来看张图理解下(下图中的 **BeanFactoryPostProcessor(工厂后处理器)** 可以忽略,在 IoC 启动过程 `refresh()` 中有涉及到,这里不提它也无所谓): ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220721190703487.png) 精简版的图示如下: ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220721190721000.png)