前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家Spring】Spring注解驱动开发---向Spring Ioc容器中注册Bean的7种方式

【小家Spring】Spring注解驱动开发---向Spring Ioc容器中注册Bean的7种方式

作者头像
YourBatman
发布2019-09-03 16:42:26
6320
发布2019-09-03 16:42:26
举报
文章被收录于专栏:BAT的乌托邦
前言

Spring是一个非常强大的反转控制(IOC)框架,以帮助分离项目组件之间的依赖关系。因此可以说Spring容器对Bean的注册、管理可以说是它的核心内容,最重要的功能部分。

因此本文主要介绍:向Spring容器注册Bean的多种方式

向Spring IOC容器注册Bean 的7种方式

所有项目建立在SpringBoot2的工程基础上构建(哪怕只用到Spring包,也用此项目构建),pom如下:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sayabc</groupId>
    <artifactId>boot2-demo1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>boot2-demo1</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
            <version>1.18.4</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
1、xml方式(老方式,现在使用得非常的少)

在resource类路径创建一个文件:beans.xml

代码语言:javascript
复制
<?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">

    <bean id="person" class="com.sayabc.boot2demo1.bean.Person">
        <property name="name" value="cuzz"></property>
        <property name="age" value="18"></property>
    </bean>

</beans>

然后main函数采用ClassPathXmlApplicationContext来启动Spring容器容器:

代码语言:javascript
复制
    public static void main(String[] args) {
        ApplicationContext applicationContext = createNewApplicationContext();
        Person bean = applicationContext.getBean(Person.class);
        System.out.println(bean); //Person(name=fsx, age=18)
    }

    //创建、启动Spring容器
    private static ApplicationContext createNewApplicationContext() {
        return new ClassPathXmlApplicationContext("classpath:beans.xml");
    }

从这便可以看出,这个bean就直接放到Spring容器里面了。

2、@Configuration @Bean配置类的方式

创建一个配置类:

代码语言:javascript
复制
/**
 * @author fangshixiang
 * @description
 * @date 2019-01-30 14:28
 */
@Configuration //该注解就相当于一个xml配置文件
public class MainConfig {

    @Bean(value = "person")
    public Person person() {
        return new Person("fsx", 18);
    }

}

这样我们使用AnnotationConfigApplicationContext来启动容器了:

代码语言:javascript
复制
    //创建、启动Spring容器
    private static ApplicationContext createNewApplicationContext() {
        return new AnnotationConfigApplicationContext(MainConfig.class);
    }

效果同上,同样能向容器中放置一个Bean。

@Bean若不指定value值,bean的id默认为方法名的名称。可以指定init-method,destroy-method方法。但是需要注意:单实例Bean容器是管理bean的init和destroy方法的,但是多实例bean容器只管帮你创建和init,之后Spring就不管了

@Bean相关注解:@Scope、@Lazy等

如果是单实例Bean,IOC容器启动时就立马创建Bean,以后获取都从容器里拿(当然你也可以加上@Lazy这个注解,让单实例Bean也懒加载)。如果是多实例Bean,Bean只有获取的时候,获取一次就创建一次。

3、使用@ComponentScan扫描注册组件

只要标注了注解就能扫描到如:@Controller @Service @Repository @component

配置类中加上这个注解:

代码语言:javascript
复制
@Configuration //该注解就相当于一个xml配置文件
@ComponentScan("com.fsx")
public class MainConfig {

}

实体类上加上一个组件组件,让其能扫描到:

代码语言:javascript
复制
@Component
public class Person {

    private String name;
    private Integer age;

}

启动Spring容器输出可以看到:

代码语言:javascript
复制
    //创建、启动Spring容器
    private static ApplicationContext createNewApplicationContext() {
        return new AnnotationConfigApplicationContext(MainConfig.class);
    }

输出为:
Person(name=null, age=null)

备注:这种扫描的方式,请保证一定要有空构造函数,否则报错的。。。

@ComponentScan有很多属性,可以实现更加精确的扫描。比如:basePackageClasses、includeFilters、excludeFilters、lazyInit、useDefaultFilters等。需要注意的是,要使includeFilters生效,需要useDefaultFilters=false才行,否则默认还是全扫

FilterType枚举的过滤类型,可以实现注解、正则等的精确匹配。当然也能CUSTOM自己实现接口来过滤,功能不可谓不强大

4、@Conditional按照条件向Spring中期中注册Bean
代码语言:javascript
复制
 /*
 * @author Phillip Webb
 * @author Sam Brannen
 * @since 4.0
 * @see Condition
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * All {@link Condition}s that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();

}

这个接口是Spirng4提供出来的。在SpringBoot底层大量的用到了这个接口来按照条件注册Bean。

从注解的属性value来看,我们可以传入Condition条件,因此我们可以传入系统自带的,也可以我们自己去实现这个接口,按照我们的需求来注册Bean

从上图可以看出,SpringBoot工程中对此接口有大量的实现。本文通过自己的实现,来看看根据条件注册Bean的强大之处。

比如我们要实现如下功能: 如果系统是windows,给容器中注入"bill",如果系统是linux,给容器中注入"linus"

代码语言:javascript
复制
  public class WindowCondition implements Condition{
  
      /**
       * @param context 判断条件
       * @param metadata 注释信息
       * @return boolean
       */
      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
          Environment environment = context.getEnvironment();
          String property = environment.getProperty("os.name");
          if (property.contains("Windows")) {
              return true;
          }
          return false;
      }
  }

需要注意的是,context还有以下方法:

代码语言:javascript
复制
  // 能获取ioc使用的beanfactory
  ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
  // 能获取到类加载器
  ClassLoader classLoader = context.getClassLoader();
  // 获取到环境变量
  Environment environment = context.getEnvironment();
  // 获取到Bean定义的注册类
  BeanDefinitionRegistry registry = context.getRegistry();

LinuxCondition类的写法略。配置类如下:

代码语言:javascript
复制
@Configuration
public class MainConfig2 {

    @Conditional({WindowCondition.class})
    @Bean("bill")
    public Person person01() {
        return new Person("Bill Gates", 60);
    }
    @Conditional({LinuxCondition.class})
    @Bean("linux")
    public Person person02() {
        return new Person("linus", 45);
    }

}

运行:(测试时候可以设置运行时参数:-Dos.name=linux

代码语言:javascript
复制
结果我们会发现,注册的Bean已经按照我们的条件去注册了

备注:@Conditonal注解不仅可以标注在方法上,还可以标注在类上。如果标注在配置类上,那么若不生效的话,这个配置类所有就将不会再生效了

5、@Improt快速导入一个组件

@Improt快速导入特别重要,在SpringBoot自动装配的过程中起到了非常关键的作用

代码语言:javascript
复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

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

}

@Import可以导入第三方包,或则自己写的类,比较方便,Id默认为全类名(这个需要注意)

比如新建一个类Color

代码语言:javascript
复制
public class Color {
}

配置类上:

代码语言:javascript
复制
@Import({Color.class})
@Configuration
public class MainConfig2 {}
6、ImportSelector和ImportBeanDefinitionRegistrar

ImportSelector:

从注解中的注释中可以看出,import除了导入具体的实体类外,还可以导入实现了指定接口的类。现在我们自己来实现一个,编写一个MyImportSelector类实现ImportSelector接口

代码语言:javascript
复制
public class MyImportSelector implements ImportSelector{

    // 返回值就导入容器组件的全类名
    // AnnotationMetadata:当前类标注的@Import注解类的所有注解信息
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {"com.cuzz.bean.Car"};
    }
}

在配置类中,通过@Import导入

代码语言:javascript
复制
@Import({Color.class, MyImportSelector.class})
@Configuration
public class MainConfig2 {}

这样子我们发现,Car类已经被导入进去了。

ImportBeanDefinitionRegistrar:

代码语言:javascript
复制
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    /**
     * @param importingClassMetadata 当前类的注解信息
     * @param registry 注册类
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 查询容器
        boolean b = registry.containsBeanDefinition("com.cuzz.bean.Car");
        // 如果有car, 注册一个汽油类
        if (b == true) {
            // 需要添加一个bean的定义信息
            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Petrol.class);
            // 注册一个bean, 指定bean名
            registry.registerBeanDefinition("petrol", rootBeanDefinition);
        }

    }
}

配置类:

代码语言:javascript
复制
@Import({Color.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
@Configuration
public class MainConfig2 {}

在SpringBoot中的使用,举个栗子: 注解@ServletComponentScan的解析,从下面代码可以看出:

代码语言:javascript
复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServletComponentScanRegistrar.class)
public @interface ServletComponentScan {}

ServletComponentScanRegistrar注册进去和解析的。在看看这个类:

代码语言:javascript
复制
class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar {}

它就是个标准的ImportBeanDefinitionRegistrar 。然后在方法registerBeanDefinitions这里面做了很多事:比如添加注解的后置处理器等等

7、使用FactoryBean注册组件

工厂Bean。此Bean非常的重要,因为第三方框架要和Spring整合,大都是通过实现此接口来实现的。

代码语言:javascript
复制
public interface FactoryBean<T> {
	T getObject() throws Exception;
	Class<?> getObjectType();
	default boolean isSingleton() {
		return true;
	}
}

举个例子,我自己来实现这个Bean接口:

代码语言:javascript
复制
public class ColorFactoryBean implements FactoryBean<Color> {
    // 返回一个Color对象
    @Override
    public Color getObject() throws Exception {
        return new Color();
    }

    @Override
    public Class<?> getObjectType() {
        return Color.class;
    }
    // 是否为单例
    @Override
    public boolean isSingleton() {
        return true;
    }
}

通过@Bean注入到容器里:

代码语言:javascript
复制
    @Bean
    public ColorFactoryBean colorFactoryBean() {
        return new ColorFactoryBean();
    }

测试一下:

代码语言:javascript
复制
    @Test
    public void test05() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);

        Object bean = applicationContext.getBean("colorFactoryBean");
        // 工厂bean调用的是getClass()方法
        System.out.println("colorFactoryBean的类型是: " + bean.getClass());
    }

输出一下,发现此时的bean调用的方法是getObjectType方法输出为:class com.fsx.boot2demo1.bean.Color

如果需要获取FactoryBean本身,可以在id前面加一个“&”标识

代码语言:javascript
复制
Object bean2 = applicationContext.getBean("&colorFactoryBean");

这个时候输出的就是:com.fsx.boot2demo1.bean.ColorFactoryBean

抛出一个问题:为何不直接使用@Bean,而使用FactoryBean呢?

Spring 中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean 即 FactoryBean。FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象。创建出来的对象是否属于单例由isSingleton中的返回决定。

官方解释: FactoryBean 通常是用来创建比较复杂的bean,一般的bean 直接用xml配置即可,但如果一个bean的创建过程中涉及到很多其他的bean 和复杂的逻辑,用xml配置比较困难,这时可以考虑用FactoryBean。

我的解释: 简单的说:它是用来处理复杂的Bean,在初始化过程中需要做很多事情(比如MyBatis的SqlSessionFactoryBean等等),从而屏蔽内部实现,对调用者/使用者友好的一种解决方案。 如果这种Bean用xml去配置,几乎是不可能的。当用注解驱动@Bean去做后,虽然也是可以的,但是很显然对调用很不友好的(因为我们使用MyBatis可不想去知道它初始化到底要做些啥事之类的)。因此使用这个方法是最优雅的解决方案。Spring在1.0就支持了这个接口,优秀~

总结

Spring提供了非常多的方式来向容器内注册Bean,从而来满足各式各样的需求。每种方式都有他独特的使用场景。比如@Bean是最长使用的,@Import导入Bean在SpringBoot的自动装配中得到了大量的使用。

一个成熟的框架很忌讳提供封闭的、不全的功能。而Spring在“开闭原则”上显然无疑是做得非常优秀的,值得深入学习

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019年01月30日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 向Spring IOC容器注册Bean 的7种方式
    • 1、xml方式(老方式,现在使用得非常的少)
      • 2、@Configuration @Bean配置类的方式
        • 3、使用@ComponentScan扫描注册组件
          • 4、@Conditional按照条件向Spring中期中注册Bean
            • 5、@Improt快速导入一个组件
              • 6、ImportSelector和ImportBeanDefinitionRegistrar
            • 7、使用FactoryBean注册组件
            • 抛出一个问题:为何不直接使用@Bean,而使用FactoryBean呢?
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档