目录

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>
1
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;
    }
}
1
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>
1
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);
    }
}
1
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();
    }
}
1
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;
    }
}
1
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();
    }
}
1
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 {
}
1
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();
    }
}
1
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 {
}
1
2
3
4

除了加载 Bean,还可以使用 @Import 注解加载配置类。其实本质上是一样的。

@Import(DogFactoryBean.class)
@Configuration
public class SpringConfig {
}
1
2
3
4

@Import自己被加载后,手动去加载别的类,当我们有顺序的加载一连串有顺序的类可以用到。

如果只是单纯想把一个类加载到容器,不会对类进行任何操作,则 @Import 也可以代替 @Bean

@Configuration
public class SpringConfig {
    @Bean
    public Cat cat(){
        return new Cat();
    }
}
1
2
3
4
5
6
7

变成

@Configuration
@Import(Cat.class)
public class SpringConfig {
}
1
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);
    }
}
1
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"};
    }
}
1
2
3
4
5
6
7
8
9
10
11

例子中的 Metadata 是获取导入这个 Selector 的配置类的源信息。可以从源信息中获取到配置类上的一些信息,可以通过这个信息进行自定义的逻辑判断来决定添加什么 Bean。

注意的是 AnnotationMetadata 在 Spring Boot 源码被大量使用(如自动装配技术),它代表元数据,那么是谁的元数据呢?是谁导入这个类,那么 AnnotationMetadata 就记录谁的元数据,如:

@Import(MyImportSelector.class)
@XXX
public class SpringConfig {
}
1
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);
    }
}
1
2
3
4
5
6
7
8
9
10

上面代码只需要知道 BookServiceImpl 是实际加载的 Bean,而 BeanDefinition 只是将 BookServiceImpl 进行封装,于是我们可以使用 BeanDefinition 的对象,针对封装的 BookServiceImpl 进行 Bean 加载控制,比如单例还是非单例:

beanDefinition.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON) // 单例
1

# 实现 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);
    }
}
1
2
3
4
5
6
7
8
9

这是最后一道关卡,不管前面怎么加载 BookServiceImpl,这里只要在加载 BookServiceImpl 的时候额外设置一些东西,那么最终在容器的 BookServiceImpl 就以这个为主。

文字无法理解,可以看视频:https://www.bilibili.com/video/BV15b4y1a7yG?p=143

看 p143 - p152,如果想了解自动装配原理,则看到 p143 - p160。

更新时间: 2024/01/17, 05:48:13
最近更新
01
JVM调优
12-10
02
jenkins
12-10
03
Arthas
12-10
更多文章>