【Spring杂烩】探讨Spring向容器注册Bean的三种方式

探讨Spring向容器注册Bean的三种方式


重点了解@Import实现的三种子方式


前提概要


Spring向容器注册Bean的三种方式

  • 通过@ComponentScan、@Componet
  • 通过@Bean方式
  • 通过@Import方式
    1. 直接导入配置类
    2. 导入ImportSelector实现类
    3. 导入ImportBeanDefiniteRegistrar实现类

三种方式的区别和应用场景


@Import与@Component、@Bean方式的区别

@Component、@Bean的区别:

  • @ComponentScan、@Componet是通过组件声明和包扫描的方式去注册Bean的,是最常用的方式之一,它提供了包扫描,批量声明组件的方式。可以让我们把所有被扫描的组件都注册到Spring容器中。@Component常用来修饰类,让其成为一个组件,一般是本工程自己定义的类,一般都会通过这种方式声明,比较简单

  • 而@Bean则通常是搭配@Configuration(跟@Component没区别)使用的,一般是在被@Component方式声明的类中去修饰方法,提供单一Bean的注册,通常用来修饰方法,把该方法的返回对象注册到Spring容器中。如果要声明的Bean的类是在第三方Jar包声明的,则该类的路径一般都不在@ComponentScan的扫描范围内,所以@Component是不会生效的,此时我们则可以通过@Bean或@Import的方式注册到Spring容器

@Import与其他的区别:

  • @Import是Spring 3.0之后才提供的,它可以快速的将一个类作为组件注册到Spring容器中,在实际开发中,通常用于配合模块装配,注册第三方组件,SpringBoot自动化装配等地方,相比前两者,可能会在普通开发中应用的少一些。但是当我们阅读Spring源码的时候,这必然是很常见的。

@Import三种子方式的区别(重点了解)

@Import方式还有三种子实现:

  1. 直接导入配置类
  2. 导入ImportSelector实现类
  3. 导入ImportBeanDefiniteRegistrar实现类

区别:

第一种直接导入配置类的方式就比较直接,直接将某个类就注册到容器了,是Spring 3.0 提供的。

第二种和第三种相比第一种方式就更加的灵活,是Spring 3.1之后提供的。

第二种方式可以在Selector实现类中按条件注册不同的组件,不如根据传入参数的,而注册不同的组件。它不像第一种方式,直接写死了一个组件。

第三种方式的话,就更加的灵活了,支持手动注册Bean,支持在BeanDefinite阶段注册组件,可以让我们在程序跑着的时候动态注册Bean, 因为它是基于BeanDefinite来操作的,可以动态代理该组件,将代理结果注册到Spring容器中。

即第一种是直接导入,第二种可以根据条件注册不同的Bean,第三种不仅可以根据条件注册不同的Bean,还可以修改Bean元数据从而达到动态的修改Bean,将被修改过的Bean注册到容器中

场景:

  • 第一种实现,直接导入配置类可以在Spring源码 - @EnableWebMvc注解中体现
  • 第二种实现,ImportSelector可以在SpringBoot 的@EnableAutoConfiguration注解中体现
  • 第三种实现,ImportBeanDefiniteRegistrar可以在SpringCloud的@EnableFeignClients注解中体现

三种方式的实现


通过@ComponentScan、@Componet注册

通过包扫描和组件声明注解的方式,是我们开发中最常见的一种Bean注册的方式:

  • 通常我们用于声明某个类为Bean,并注册到Spring容器中
  • SpringBoot中@ComponentScan,被声明在引导类的@SpringBootApplication注解中
  • 效果等同于XML配置文件中的

原理就是:

  • 首先,通过@Componet注解组件类
  • 然后,配置@ComponentScan注解,扫描需要注册的组件
常用属性名 类型 说明
includeFilters Filter[] 指定扫描导入类型的过滤规则
excludeFilters Filter[] 指定扫描排除类型的过滤规则

也许你还会问,通常我们不是也使用了@Service,@Configuration,@Controller等模式注解,这些也算是这种方式注册的吗?

答案是肯定,因为这些注解都仅仅是@Component组件的派生,仅仅是名称不同的@Component , 名字是起标识性作用的,真正发挥作用的还是@Component注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
     
}

比如@Configuration注解,我们可以看到,里面就声明了一个@Componet注解,然后就没有什么特别的了


通过@Bean方式注册

通过@Bean方式注册Bean也是我们开发中常用的一种开发方式:

  • @Bean可以用于修饰方法和注解
  • 通常会把@Bean修饰的方法看做是生成该Bean的来源途经
  • 通常我们也是在@Configuration所修饰的配置类中声明@Bean
  • 效果等同于XML配置文件中的

最常见的用法就是:

  • 标记在方法上,将方法返回值注册到Spring容器,类型为返回值类型,bean id默认为方法名

比如

@Configuration
public class MyConfig(){
     

	@Bean 
	public String hello(){
     
		return "hello world";
	}

}

这里会生成一个id为hello, 值为hello world的String对象,这个对象被声明为Bean并注册到Spring容器中


通过@Import方式注册

@Import方式的使用也比较频繁,更多的情况下,我们会在阅读Spring框架源码的时候遇到

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
     

	/**
	 * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
	 * or regular component classes to import.
	 */
	Class<?>[] value();

}

从源码的中,我们可以看到:

  • 里面只有一个成员属性,Class数组类型变量 value
  • @Import可以修饰类,注解等…

从注释中,我们可以看出来,value通常有三种选择:

  • 直接指向一个配置类(比如@Configuration)
  • 直接指向一个ImportSelector接口实现类
  • 直接指向一个ImportBeanDefinitionRegistrar实现类

小结:

  • @Import可以修饰类和注解等
  • @Import可以指定一个或多个类,这些类将被直接注册到Spring容器中,这些类可以不需要其他注解的修饰,比如@Configuration
  • @Import指定的value属性大致可以分为三类,普通配置类,ImportSelector实现类,ImportBeanDefinitionRegistrar 实现类,后两者是Spring 3.1后提供的可选方式,前者是Spring 3.0 提供的方式,后者相对前者而言,更加的灵活

@Import直接导入Bean

第一步,自定义注解EnableHello

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyConfiguration.class)
public @interface EnableHello{
     
}

第二步,自定义配置类

public class MyConfig{
     
}

第三步,声明注解

@EnableHello
@SpringBootApplication
public class DemoApplication {
     

    public static void main(String[] args) {
     
        SpringApplication.run(DemoApplication.class, args);
    }
}

通过在某个组件,比如SpringBoot启动引导类中声明@EnableHello注解,那么效果就是MyConfig普通配置类将直接被注册到Spring容器中


实现ImportSelector接口,返回Bean类名数组

第一步,自定义注解EnableHello

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyConfiguration.class)
public @interface EnableHello{
     
}

第二步,自定义配置类

public class MyConfig{
     
}

第三步,自定义xxxSelector实现类

public class HelloImportSelector implements ImportSelector {
     
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
     
        return new String[]{
     MyConfig.class.getName()};
        //返回所需配置类的全限定名称 ,比如com.example.demo.config.MyConfig
    }
}

第三步,声明注解

@EnableHello
@SpringBootApplication
public class DemoApplication {
     

    public static void main(String[] args) {
     
        SpringApplication.run(DemoApplication.class, args);
    }
}

我们这里并没有根据条件来注册不同的Bean,但是可以知道,自定义注解中可以定义一个value,在selectImports()方法中,我们可以拿到注解的元数据,从而得到value是什么,根据这个value就可以返回对应要注册的Bean名称数组


实现ImportBeanDefinitionRegistrar 接口,自定义Bean注册

第一步,声明注解,@Import执行我们的ImportBeanDifinitionRegistrar实现类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyImportBeanDefinitionRegistrar .class)
public @interface EnableHello{
     
}

第二步,声明一个类,准备作为Bean,但没有声明注册, 属性name默认没有值

@Data
public class Hello {
     

    private String name;

}

第三步,自定义ImportBeanDifinitionRegistrar实现类

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
     
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
     
    	//生成Hello类的BeanDefinition
        BeanDefinition beanDefinition = new RootBeanDefinition(Hello.class);
        //获取Hello元数据中的属性参数
        PropertyValues propertyValues = beanDefinition.getPropertyValues();
        //因为name默认为空,所以赋予值
        ((MutablePropertyValues) propertyValues).add("name","snailmann");
        //将修改过的Bean元数据注册到BeanDefinitionRegistry中
        registry.registerBeanDefinition("hello",beanDefinition);
    }
}

第四步,启动类声明自定义注解

@EnableHello
@SpringBootApplication
public class Demo4Application {
     

    public static void main(String[] args) {
     
        ApplicationContext context = SpringApplication.run(Demo4Application.class, args);
        Hello hello = (Hello) context.getBean("hello");
        System.out.println(hello);

    }

}

输出结果为:

Hello(name=jerry)

从这里,我们就可以知道,ImportBeanDifinitionRegistrar的功能是最牛逼的,不仅仅可以实现Selector的功能,还可以在运行期间动态的修改Bean的元数据,从而动态注册Bean;

就像我们知道@EnableFeignClients注解就是通过这样的方式实现的。我得猜想就是,@FeignClient声明API , 然后@EnableFeignClients指定的register会寻找带有@FeignClient的类。并修改这个类的Bean元数据,有点像动态代理的感觉。而因为这些api就是空壳方法,所以register就是去为其实现http请求的功能


小结
  • 小细节就是我们并没有在配置类,ImportSelector实现类,ImportBeanDefinitionRegistrar 实现类中声明@Configuration,@Compenent等模式注解,这就说明,@Import并不需要模式注解的帮助,这是一套独立的注册方式。

参考资料


  • SpringBoot-向容器注册Bean的多种方式 - 作者: 东京易冷
  • 动态注册bean,Spring官方套路:使用ImportBeanDefinitionRegistrar - 作者:李佳明
  • 借助ImportBeanDefinitionRegistrar接口实现bean的动态注入 - 作者:Top_Bear

你可能感兴趣的:(Spring,Spring,Bean)