Spring Boot - Bean 加载方式
介绍 Bean 的八种加载方式
# 配置文件 + <bean/>
标签
古老的记忆袭击,第一种方式就是给出 Bean 的类名,内部通过反射机制加载成 Class。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- xml 方式声明自己开发的 bean-->
<bean id="cat" class="Cat"/>
<bean class="Dog"/>
<!-- xml 方式声明第三方开发的 bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>
2
3
4
5
6
7
8
9
10
11
12
13
# 配置文件扫描 + 注解定义 Bean
这里可以使用的注解有 @Component
以及三个衍生注解 @Service
、@Controller
、@Repository
。
@Component("tom")
public class Cat {
}
@Service
public class Mouse {
}
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
从前从前,是通过配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定扫描加载 Bean 的位置-->
<context:component-scan base-package="com.itheima.bean,com.itheima.config"/>
</beans>
2
3
4
5
6
7
8
9
10
11
12
13
现在可以直接在启动类加启动注解 @SpringBootApplication
:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(GeneratorApplication.class, args);
}
}
2
3
4
5
6
注意的是该文件所在的目录层级优先级最高。
# 注解方式声明配置类
使用 @ComponentScan
扫描指定的包路径下的带有 @Component
以及三个衍生注解 @Service
、@Controller
、@Repository
的类。
@ComponentScan({"cn.youngkbt.bean","cn.youngkbt.config"})
public class SpringConfig {
@Bean
public DogFactoryBean dog(){
return new DogFactoryBean();
}
}
2
3
4
5
6
7
值得一提的是,这里也使用了另一个注解 @Bean
,将方法的结果作为 Bean 传入 Spring 容器,前提是 SpringConfig 被加载为 Bean。
再值得一提的一个知识是,DogFactoryBean 类使用了 FactoryBean 接口。
Spring 提供了一个接口 FactoryBean,也可以用于声明 Bean,只不过实现了 FactoryBean 接口的类造出来的对象不是当前类的对象,而是 FactoryBean 接口泛型指定类型的对象。如下列,造出来的 Bean 并不是 DogFactoryBean,而是 Dog。
public class DogFactoryBean implements FactoryBean<Dog> {
@Override
public Dog getObject() throws Exception {
Dog d = new Dog();
//.........
return d;
}
@Override
public Class<?> getObjectType() {
return Dog.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
等价于:
@ComponentScan({"cn.youngkbt.bean","cn.youngkbt.config"})
public class SpringConfig {
@Bean
public Dog dog(){
return new Dog();
}
}
2
3
4
5
6
7
有人说,注释中的代码直接写入 Dog 的构造方法不就行了吗?干嘛这么费劲转一圈,还写个类,还要实现接口,多麻烦啊。还真不一样,你可以理解为 Dog 是一个抽象后剥离的特别干净的模型,但是实际使用的时候必须进行一系列的初始化动作。只不过根据情况不同,初始化动作不同而已。如果写入 Dog,或许初始化动作 A 当前并不能满足你的需要,这个时候你就要做一个 DogB 的方案了。你就要做两个 Dog 类。当时使用 FactoryBean 接口就可以完美解决这个问题。
# 番外
# 导入 XML 格式配置的 Bean
由于早起开发的系统大部分都是采用 XML 的形式配置 Bean,现在的企业级开发基本上不用这种模式了。 但是如果你特别幸运,需要基于之前的系统进行二次开发,这就尴尬了。新开发的用注解格式,之前开发的是 XML 格式。这个时候可不是让你选择用哪种模式的,而是两种要同时使用。
Spring 提供了一个注解可以解决这个问题,@ImportResource
,在配置类上直接写上要被融合的 XML 配置文件名即可,算的上一种兼容性解决方案。
@Configuration
@ImportResource("applicationContext.xml")
public class SpringConfig {
}
2
3
4
这将会去 application.yml 根目录下读取 applicationContext.xml
文件。
# proxyBeanMethods 属性
@Configuration
这个注解,当我们使用 AnnotationConfigApplicationContext 加载配置类的时候,配置类可以不添加这个注解。但是这个注解有一个更加强大的功能,它可以保障配置类中使用方法创建的 Bean 的唯一性。为 @Configuration
注解设置 proxyBeanMethods 属性值为 true 即可,此属性默认值为 true。
当 proxyBeanMethods 为 true,则为 Full 模式,反之为 Lite 模式。
/**
* 1、配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理 Bean 的方法
* Full(proxyBeanMethods = true)、【保证每个 @Bean 方法被调用多少次返回的组件都是单实例的】
* Lite(proxyBeanMethods = false)【每个 @Bean 方法被调用多少次返回的组件都是新创建的】
* 组件依赖必须使用 Full 模式默认。其他默认是否 Lite 模式
*/
@Configuration(proxyBeanMethods = true)
public class SpringConfig {
@Bean
public Cat cat(){
return new Cat();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
什么叫做保证 Bean 的唯一性呢?
首先我们知道 Configuration
修饰某个类后,该类里的方法带有 @Bean
注解后,那么 Spring 会将该方法的返回值作为 Bean 注入到 Spring 容器里。
proxyBeanMethods 为 true 时,我们使用 @Autowired
或其他方法自动注入类的时候,该类将从 Spring 容器里获取,也就是单例。
但是 proxyBeanMethods 为 false 时,我们每次注入的都是一个全新的类,也就是说,Spring 每次注入的时候都会执行 @Bean
修饰的方法,拿到返回值来执行注入。而不是从 Spring 容器找出已经存在的该类来注入。
因此,proxyBeanMethods 控制 Spring 是从容器获取存在的类还是调用对应的方法得到返回值来返回类。
# 使用 @Impore 注入 Bean
使用扫描的方式加载 Bean 是企业级开发中常见的 Bean 的加载方式,但是由于扫描的时候不仅可以加载到你要的东西,还有可能加载到各种各样的乱七八糟的东西。
比如你扫描了 cn.youngkbt.service
包,后来因为业务需要,又扫描了 cn.youngkbt.dao
包,你发现 cn.youngkbt
包下面有 service 和 dao 这两个包,这就简单了,直接扫描 cn.youngkbt
就行了。但是万万没想到,十天后你加入了一个外部依赖包,里面也有 cn.youngkbt
包,这下就热闹了,该来的不该来的全来了。
所以我们需要一种精准制导的加载方式,使用 @Import
注解就可以解决你的问题。它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的 .class
即可。
@Import({Dog.class, DbConfig.class}) // 当 SpringConfig 加载后,也会加载 Dog、DbConfig 类
@Configuration
public class SpringConfig {
}
2
3
4
除了加载 Bean,还可以使用 @Import
注解加载配置类。其实本质上是一样的。
@Import(DogFactoryBean.class)
@Configuration
public class SpringConfig {
}
2
3
4
@Import
在 自己被加载后,手动去加载别的类,当我们有顺序的加载一连串有顺序的类可以用到。
如果只是单纯想把一个类加载到容器,不会对类进行任何操作,则 @Import
也可以代替 @Bean
@Configuration
public class SpringConfig {
@Bean
public Cat cat(){
return new Cat();
}
}
2
3
4
5
6
7
变成
@Configuration
@Import(Cat.class)
public class SpringConfig {
}
2
3
4
# 编程形式注册 Bean
前面介绍的加载 Bean 的方式都是在容器启动阶段完成 Bean 的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载 Bean。通过这种方式可以实现编程式控制 Bean 的加载。
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 上下文容器对象已经初始化完毕后,手工加载 Bean
ctx.register(Mouse.class);
}
}
2
3
4
5
6
7
# 实现 ImportSelector 接口类
实现 ImportSelector 接口的类可以设置加载的 Bean 的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 各种条件的判定,判定完毕后,决定是否装载指定的bean
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if(flag){
return new String[]{"cn.youngkbt.bean.Dog"};
}
return new String[]{"cn.youngkbt.bean.Cat"};
}
}
2
3
4
5
6
7
8
9
10
11
例子中的 Metadata 是获取导入这个 Selector 的配置类的源信息。可以从源信息中获取到配置类上的一些信息,可以通过这个信息进行自定义的逻辑判断来决定添加什么 Bean。
注意的是 AnnotationMetadata 在 Spring Boot 源码被大量使用(如自动装配技术),它代表元数据,那么是谁的元数据呢?是谁导入这个类,那么 AnnotationMetadata 就记录谁的元数据,如:
@Import(MyImportSelector.class)
@XXX
public class SpringConfig {
}
2
3
4
SpringConfig 类手动加载 MyImportSelector 类,那么 MyImportSelector 类的 AnnotationMetadata 就记录者 SpringConfig 的信息,就可以使用 AnnotationMetadata 获取 SpringConfig 的 XXX 注解等该类的基本信息。
用这种方式来加载 Bean 会更灵活,适用于加载的 Bean 需要通过一些条件判断后来决定是否加载的场景。
# 实现 ImportBeanDefinitionRegistrar 接口类
方式六中提供了给定类全路径类名控制 Bean 加载的形式, 其实 Bean 的加载不是一个简简单单的对象,Spring 中定义了一个叫做 BeanDefinition 的东西,它才是控制 Bean 初始化加载的核心。BeanDefinition 接口中给出了若干种方法,可以控制 Bean 的相关属性。说个最简单的,创建的对象是单例还是非单例,在 BeanDefinition 中定义了 scope 属性就可以控制这个。如果你感觉方式六没有给你开放出足够的对 Bean 的控制操作,那么方式七你值得拥有。
我们可以通过定义一个类,然后实现 ImportBeanDefinitionRegistrar 接口的方式定义 Bean:
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 加载 BookServiceImpl 类
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl.class).getBeanDefinition();
// 注册类,key 是类在 Spring 容器的 id
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
2
3
4
5
6
7
8
9
10
上面代码只需要知道 BookServiceImpl 是实际加载的 Bean,而 BeanDefinition 只是将 BookServiceImpl 进行封装,于是我们可以使用 BeanDefinition 的对象,针对封装的 BookServiceImpl 进行 Bean 加载控制,比如单例还是非单例:
beanDefinition.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON) // 单例
# 实现 BeanDefinitionRegistryPostProcessor 接口类
上述七种方式都是在容器初始化过程中进行 Bean 的加载或者声明,但是这里有一个 Bug。这么多种方式,它们之间如果有冲突怎么办?谁能有最终裁定权?
BeanDefinitionRegistryPostProcessor,看名字知道,BeanDefinition 意思是 Bean 定义,Registry 注册的意思,Post 后置,Processor 处理器,全称 Bean 定义后处理器,干啥的?在所有 Bean 注册都折腾完后,它是最后一道关卡,说白了,它说了算,所以它是最后一个运行的。
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 加载 BookServiceImpl 类
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl.class).getBeanDefinition();
// 注册类,key 是类在 Spring 容器的 id
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
2
3
4
5
6
7
8
9
这是最后一道关卡,不管前面怎么加载 BookServiceImpl,这里只要在加载 BookServiceImpl 的时候额外设置一些东西,那么最终在容器的 BookServiceImpl 就以这个为主。
文字无法理解,可以看视频:https://www.bilibili.com/video/BV15b4y1a7yG?p=143
。
看 p143 - p152,如果想了解自动装配原理,则看到 p143 - p160。