Springboot | bean的加载方式


ps:本文为作者学习笔记技术参考意义不大


一. 自动配置工作流程

自动配置是springboot技术非常好用的核心因素,前面学习了这么多种技术的整合,每一个都离不开自动配置。不过在学习自动配置的时候,需要你对spring容器如何进行bean管理的过程非常熟悉才行,所以这里需要先复习一下有关spring技术中bean加载相关的知识。方式方法很多,逐一快速复习一下,查漏补缺。不过这里需要声明一点,这里列出的bean的加载方式仅仅应用于后面课程的学习,并不是所有的spring加载bean的方式。跟着我的步伐一种一种的复习,他们这些方案之间有千丝万缕的关系,顺着看完,你就懂自动配置是怎么回事了。

1. 准备工作

首先我们先要创建好一个基础的java模块,在POM文件中添加下面的依赖信息:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.9</version>
</dependency>

创建一个BookService接口,以及其四个实现类,定义方法分别输出不同的内容:

public interface BookSerivce {
    
    
    void check();
}
public class BookServiceImpl1 implements BookSerivce {
    
    
    @Override
    public void check() {
    
    
        System.out.println("book service 1..");
    }
}
// 四个实现类以此类推

在这里插入图片描述

二. Bean的加载方式

1. 方式一:xml方式

创建一个Spring的配置文件,在其中使用xml的方式定义bean:
在这里插入图片描述
在这里插入图片描述

定义方式

注意定义第三方的bean时要确保第三方的依赖包导入到项目中

<?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="com.itheima.bean.Cat"/>
    <bean class="com.itheima.bean.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>

获取方式

public class App1 {
    
    
    public static void main(String[] args) {
    
    
    	// applicationCOntext1.xml Spring的配置文件文件名
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationCOntext1.xml");
		// Object cat = ctx.getBean("cat"); // 用id拿
		// System.out.println(cat);
		// Dog dog = ctx.getBean(Dog.class); // 用类型拿
		// System.out.println(dog);
        String[] names = ctx.getBeanDefinitionNames(); // 拿到的所有bean的名称
        for (String name : names) {
    
    
            System.out.println(name);
        }
    }
}

如果有多个相同的bean打印出来的bean类路径会以#数字递增的方式区分:
在这里插入图片描述

2. 方式二:注解方式结合配置文件

上一种定义的方式过于繁琐,不如使用注解来定义bean来的方便可以使用@Component注解直接电话已,还有三个衍生注解也有相同的效果:

  • @Controller:用于表现层bean定义
  • @Service:用于业务层bean定义
  • @Repository:用于数据层bean定义

定义方式

如果想将该类定义为bean只需要在类上面添加注解就可以了:

@Component("tom") // 注解value的值就是bena的id
public class Cat {
    
    
    public Cat(){
    
    
    }

    int age;
    public Cat(int age){
    
    
        this.age = age;
    }

    @Override
    public String toString() {
    
    
        return "Cat{" +
                "age=" + age +
                '}';
    }
}

这样的方式那么第三方的类应该怎么去定义呢?

@Configuration // 标注为配置类 这个也附带加载为bena的效果
public class DbConfig {
    
    

    @Bean
    public DruidDataSource dataSource(){
    
    
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }

}

但是这样的方式需要指定bean的扫描路径:
applicationCOntext2.xml内容:

<?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>

获取方式

public class App2 {
    
    
    public static void main(String[] args) {
    
    
    	// 定义扫描路劲的配置文件名
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationCOntext2.xml");
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
    
    
            System.out.println(name);
        }
    }
}

3. 方式三:注解结合配置类

上一种定义方式任然存在一个问题,那就是配置文件已经存在,现在这种方法就是使用类来代替这个配置文件。

定义方式

首先我们创建一个全新的配置类SpringConfig3内容如下:

// 代替spring的配置文件
@ComponentScan({
    
    "com.itheima.bean","com.itheima.config"}) // 扫描的配置组件的
public class SpringConfig3 {
    
    

}

获取方式

public class App3 {
    
    
    public static void main(String[] args) {
    
    
        // 没有配置文件了 直接加载配置类
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig3.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
    
     
            System.out.println(name);
        }
    }
}

4. 方式四:@Import注解

精准制导的加载方式,使用@Import注解就可以解决你的问题。它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的.class即可。有人就会觉得,还要自己手写,多麻烦,不如扫描好用。对呀,但是他可以指定加载啊,好的命名规范配合@ComponentScan可以解决很多问题,但是@Import注解拥有其重要的应用场景。有没有想过假如你要加载的bean没有使用@Component修饰呢?这下就无解了,而@Import就无需考虑这个问题。

定义方式

这个Dog类和Cat都不需要被@Component注解标注

@Import({
    
    Dog.class,Cat.class})
public class SpringConfig4 {
    
    
}

处了导入类,还可以直接导入别的配置类,这样配置类中的bena也会被加载进去。

@Import(DbConfig.class)
public class SpringConfig4 {
    
    
}

且配置类也不需要注解:

public class DbConfig {
    
    
	
	// 也会被加载
    @Bean
    public DruidDataSource dataSource(){
    
    
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }

}

获取方式

public class App4 {
    
    
    public static void main(String[] args) {
    
    
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
    
    
            System.out.println(name);
        }
    }
}

5. 方式五:编程形式

前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。

定义方式

public class App5 {
    
    
    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //上下文容器对象已经初始化完毕后,手工加载bean
        ctx.register(Mouse.class);
    }
}

其实这种方式坑还是挺多的,比如容器中已经有了某种类型的bean,再加载会覆盖。

public class App5 {
    
    
    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //上下文容器对象已经初始化完毕后,手工加载bean
        // 参数一: id
       	// 参数二: 类的字节码
       	// 参数三: 类的有参构造器参数
        ctx.registerBean("tom", Cat.class,0);
        ctx.registerBean("tom", Cat.class,1);
        ctx.registerBean("tom", Cat.class,2);
        System.out.println(ctx.getBean(Cat.class));
    }
}

获取方式

public class App5 {
    
    
    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //上下文容器对象已经初始化完毕后,手工加载bean
        // 参数一: id
       	// 参数二: 类的字节码
       	// 参数三: 类的有参构造器参数
        ctx.registerBean("tom", Cat.class,0);
        ctx.registerBean("tom", Cat.class,1);
        ctx.registerBean("tom", Cat.class,2);
        System.out.println(ctx.getBean(Cat.class));
    }
}

6. 方式六:ImportSelector接口

在方式五种,我们感受了bean的加载可以进行编程化的控制,添加if语句就可以实现bean的加载控制了。但是毕竟是在容器初始化后实现bean的加载控制,那是否可以在容器初始化过程中进行控制呢?答案是必须的。实现ImportSelector接口的类可以设置加载的bean的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。

定义方式

public class  MyImportSelector implements ImportSelector {
    
    
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
    
    
//        System.out.println("================");
//        System.out.println("提示:"+metadata.getClassName()); // 加载的是哪个bean描述的就是哪个
//        System.out.println(metadata.hasAnnotation("org.springframework.context.annotation.Configuration"));// 加载这个类的 配置文件有没有这个注解 输出布尔值
//        Map<String, Object> attributes = metadata.getAnnotationAttributes("org.springframework.context.annotation.ComponentScan");
//        System.out.println(attributes); // 拿到很多这个注解的值
//        System.out.println("================");

        //各种条件的判定,判定完毕后,决定是否装在指定的bean
        boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
        if(flag){
    
    
            return new String[]{
    
    "com.itheima.bean.Dog"}; // 返回的是要注册bean的全类名
        }
        return new String[]{
    
    "com.itheima.bean.Cat"}; // 要定义的bean的全路径
    }
}

@Configuration
//@ComponentScan(basePackages = "com.itheima")
@Import(MyImportSelector.class)
public class SpringConfig6 {
    
    
}

获取方式

public class App6 {
    
    
    public static void main(String[] args) {
    
    
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig6.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
    
    
            System.out.println(name);
        }
        System.out.println("----------------------");
    }
}

7. 方式七:ImportBeanDefinitionRegistrar接口

方式六中提供了给定类全路径类名控制bean加载的形式,如果对spring的bean的加载原理比较熟悉的小伙伴知道,其实bean的加载不是一个简简单单的对象,spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心。BeanDefinition接口中给出了若干种方法,可以控制bean的相关属性。说个最简单的,创建的对象是单例还是非单例,在BeanDefinition中定义了scope属性就可以控制这个。如果你感觉方式六没有给你开放出足够的对bean的控制操作,那么方式七你值得拥有。我们可以通过定义一个类,然后实现ImportBeanDefinitionRegistrar接口的方式定义bean,并且还可以让你对bean的初始化进行更加细粒度的控制,不过对于新手并不是很友好。忽然给你开放了若干个操作,还真不知道如何下手。

定义方式

public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    
    
	// 参数一: 和上个方法的一样
	// 参数二: 
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    
    
        // 创建的bean BookServiceImpl2.class
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
        registry.registerBeanDefinition("bookService",beanDefinition);
        // bookService 是bean的id
    }
}
@Import(MyRegistrar.class)
public class SpringConfig7 {
    
    
}

获取方式

public class App7 {
    
    
    public static void main(String[] args) {
    
    
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig7.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
    
    
            System.out.println(name);
        }
        System.out.println("----------------------");
    }
}

8. 方式八:BeanDefinitionRegistryPostProcessor接口的类

定义方式

public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    
    
	// 参数一: 和上一种方法一样
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    
    
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
        registry.registerBeanDefinition("bookService",beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    
    

    }
}
// 正常情况那边拿到的是最后一个bean  但是MyPostProcessor.class 配置的会优先拿到
@Import({
    
    BookServiceImpl1.class, MyPostProcessor.class, MyRegistrar2.class, MyRegistrar.class})
public class SpringConfig8 {
    
    
}

获取方式

public class App8 {
    
    
    public static void main(String[] args) {
    
    
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig8.class);
        BookSerivce bookService = ctx.getBean("bookService", BookSerivce.class);// 按照名字和接口拿
        bookService.check();
    }
}

三. 其他

1. 工厂bean

首先我们先创建一个Dog类:

public class Dog {
    
    
}

接着我们可以创建这个Dog类对应的工厂类:

import org.springframework.beans.factory.FactoryBean;
// 工厂bean  这个泛型就是要创建的bean的类型
public class DogFactoryBean implements FactoryBean<Dog> {
    
    
	// 实现这个工厂bean造出来的对象是什么
    @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; 
        // 返回true的话造出的就是同一个bean
        // 返回false的话造成的就不是同一个bean
        // 这就是单例 和 非单例
    }
}

配置类内容:

// 代替spring的配置文件
@ComponentScan({
    
    "com.itheima.bean","com.itheima.config"}) // 扫描的配置组件的
public class SpringConfig3 {
    
    

    @Bean // id就是这个方法的名字
    public DogFactoryBean dog(){
    
    
        return new DogFactoryBean(); // 获取bean的时候 获取的还是一个dog对象因为这是个工厂类
    }

}

2. 注解格式导入XML格式配置的bean

由于早起开发的系统大部分都是采用xml的形式配置bean,现在的企业级开发基本上不用这种模式了。但是如果你特别幸运,需要基于之前的系统进行二次开发,这就尴尬了。新开发的用注解格式,之前开发的是xml格式。这个时候可不是让你选择用哪种模式的,而是两种要同时使用。spring提供了一个注解可以解决这个问题,@ImportResource,在配置类上直接写上要被融合的xml配置文件名即可,算的上一种兼容性解决方案,没啥实际意义。
我们创建一个配置类:

@Configuration
@ImportResource("applicationContext1.xml") // 将配置文件中的bean也加载到这个配置类
public class SpringConfig32 {
    
    
}

3. proxyBeanMethods属性

前面的例子中用到了@Configuration这个注解,当我们使用AnnotationConfigApplicationContext加载配置类的时候,配置类可以不添加这个注解。但是这个注解有一个更加强大的功能,它可以保障配置类中使用方法创建的bean的唯一性。 为@Configuration注解设置proxyBeanMethods属性值为true即可,由于此属性默认值为true,所以很少看见明确书写的,除非想放弃此功能。

// proxyBeanMethods = true 获取bean的时候获取到的都是同一个bean
// proxyBeanMethods = false 获取bean的时候获取的是不同的bean
@Configuration(proxyBeanMethods = true)
public class SpringConfig33 {
    
    
    @Bean
    public Cat cat(){
    
    
        return new Cat();
    }
}

猜你喜欢

转载自blog.csdn.net/Siebert_Angers/article/details/128881934