大家好,我是三友~~

这篇文章我准备来扒一扒Bean注入到Spring的那些姿势。

其实关于Bean注入Spring容器的方式网上也有很多相关文章,但是很多文章可能会存在以下常见的问题

  • 注入方式总结的不全
  • 没有分析可以使用这些注入方式背后的原因
  • 没有这些注入方式在源码中的应用示例
  • ...

所以本文就带着解决上述的问题的目的来重新梳理一下Bean注入到Spring的那些姿势。

配置文件

配置文件的方式就是以外部化的配置方式来声明Spring Bean,在Spring容器启动时指定配置文件。配置文件方式现在用的不多了,但是为了文章的完整性和连续性,这里我还是列出来了,知道的小伙伴可以自行跳过这节。

配置文件的类型Spring主要支持xml和properties两种类型。

xml

在XmlBeanInjectionDemo.xml文件中声明一个class为类型为User的Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       ">

    <bean class="com.sanyou.spring.bean.injection.User"/>

</beans>

User

@Data
@ToString
public class User {

    private String username;

}

测试:

public class XmlBeanInjectionDemo {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:XmlBeanInjectionDemo.xml");
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

结果:

User(username=null)

可以看出成功将User注入到Spring中,由于没有设置username属性值,所以是null。

properties

除了xml,spring还支持properties配置文件声明Bean的方式。

如下,在PropertiesBeanInjectionDemo.properties文件中声明了class类型为User的Bean,并且设置User的username属性为sanyou。

user.(class) = com.sanyou.spring.bean.injection.User
user.username = sanyou

测试:

public class PropertiesBeanInjectionDemo {

    public static void main(String[] args) {
        GenericApplicationContext applicationContext = new GenericApplicationContext();
        //创建一个PropertiesBeanDefinitionReader,可以从properties读取Bean的信息,将读到的Bean信息放到applicationContext中
        PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(applicationContext);
        //创建一个properties文件对应的Resource对象
        Resource classPathResource = new ClassPathResource("PropertiesBeanInjectionDemo.properties");
        //加载配置文件
        propReader.loadBeanDefinitions(classPathResource);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

结果:

User(username=sanyou)

成功获取到User对象,并且username的属性为properties设置的sanyou。

除了可以配置属性之外还支持其它的配置,如何配置可以查看PropertiesBeanDefinitionReader类上的注释。

注解声明

上一节介绍了通过配置文件的方式来声明Bean,但是配置文件这种方式最大的缺点就是不方便,因为随着项目的不断扩大,可能会产生大量的配置文件。为了解决这个问题,Spring在2.x的版本中开始支持注解的方式来声明Bean。

@Component + @ComponentScan

这种方式其实就不用多说,在项目中自定义的业务类就是通过@Component及其派生注解(@Service、@Controller等)来注入到Spring容器中的。

在SpringBoot环境底下,一般情况下不需要我们主动调用@ComponentScan注解,因为@SpringBootApplication会调用@ComponentScan注解,扫描启动引导类(加了@SpringBootApplication注解的类)所在的包及其子包下所有加了@Component注解及其派生注解的类,注入到Spring容器中。

@Bean

虽然上面@Component + @ComponentScan的这种方式可以将Bean注入到Spring中,但是有个问题那就是对于第三方jar包来说,如果这个类没加@Component注解,那么@ComponentScan就扫不到,这样就无法注入到Spring容器中,所以Spring提供了一种@Bean的方式来声明Bean。

比如,在使用MybatisPlus的分页插件的时候,就可以按如下方式这么来声明。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}

此时就能将MybatisPlusInterceptor这个Bean注入到Spring容器中。

@Import

@Import注解也可以用来将Bean注入到Spring容器中,@Import注解导入的类可以分为三种情况:

  • 普通类
  • 类实现了ImportSelector接口
  • 类实现了ImportBeanDefinitionRegistrar接口
普通类

普通类其实就很简单,就是将@Import导入的类注入到Spring容器中,这没什么好说的。

类实现了ImportSelector接口
public interface ImportSelector {

    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }

}

当@Import导入的类实现了ImportSelector接口的时候,Spring就会调用selectImports方法的实现,获取一批类的全限定名,最终这些类就会被注册到Spring容器中。

比如如下代码中,UserImportSelector实现了ImportSelector,selectImports方法返回User的全限定名

public class UserImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("调用 UserImportSelector 的 selectImports 方法获取一批类限定名");
        return new String[]{"com.sanyou.spring.bean.injection.User"};
    }

}

当使用@Import注解导入UserImportSelector这个类的时候,其实最终就会把User注入到Spring容器中,如下测试

@Import(UserImportSelector.class)
public class ImportSelectorDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将 ImportSelectorDemo 注册到容器中
        applicationContext.register(ImportSelectorDemo.class);
        applicationContext.refresh();

        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

运行结果

User(username=null)

对于类实现了ImportBeanDefinitionRegistrar接口的情况,这个后面说。

一般来说,@Import都是配合@EnableXX这类注解来使用的,比如常见的@EnableScheduling、@EnableAsync注解等,其实最终都是靠@Import来实现的。


@EnableScheduling

@EnableAsync

讲完通过注解的方式来声明Bean之后,可以来思考一个问题,那就是既然注解方式这么简单,为什么Spring还写一堆代码来支持配置文件这种声明的方式?

其实答案很简单,跟Spring的发展历程有关。Spring在创建之初Java还不支持注解,所以只能通过配置文件的方式来声明Bean,在Java1.5版本开始支持注解之后,Spring才开始支持通过注解的方式来声明Bean。

注册BeanDefinition

在说注册BeanDefinition之前,先来聊聊什么是BeanDefinition?

BeanDefinition是Spring Bean创建环节中很重要的一个东西,它封装了Bean创建过程中所需要的元信息。

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    //设置Bean className
    void setBeanClassName(@Nullable String beanClassName);

    //获取Bean className
    @Nullable
    String getBeanClassName();
    
    //设置是否是懒加载
    void setLazyInit(boolean lazyInit);

    //判断是否是懒加载
    boolean isLazyInit();
    
    //判断是否是单例
    boolean isSingleton();

}

如上代码是BeanDefinition接口的部分方法,从这方法的定义名称可以看出,一个Bean所创建过程中所需要的一些信息都可以从BeanDefinition中获取,比如这个Bean的class类型,这个Bean是否是懒加载,这个Bean是否是单例的等等,因为有了这些信息,Spring才知道要创建一个什么样的Bean。

有了BeanDefinition这个概念之后,再来看一下配置文件和注解声明这些方式往Spring容器注入Bean的原理。


Bean注入到Spring原理

如图为Bean注入到Spring大致原理图,整个过程大致分为以下几个步骤

  • 通过BeanDefinitionReader组件读取配置文件或者注解的信息,为每一个Bean生成一个BeanDefinition
  • BeanDefinition生成之后,添加到BeanDefinitionRegistry中,BeanDefinitionRegistry就是用来保存BeanDefinition
  • 当需要创建Bean对象时,会从BeanDefinitionRegistry中拿出需要创建的Bean对应的BeanDefinition,根据BeanDefinition的信息来生成Bean
  • 当生成的Bean是单例的时候,Spring会将Bean保存到SingletonBeanRegistry中,也就是平时说的三级缓存中的第一级缓存中,以免重复创建,需要使用的时候直接从SingletonBeanRegistry中查找

好了,通过以上分析我们知道,配置文件和注解声明的方式其实都是声明Bean的一种方式,最终都会转换成BeanDefinition,Spring是基于BeanDefinition的信息来创建Bean。

既然Spring最终是基于BeanDefinition的信息来创建Bean,那么我们是不是可以跳过配置文件和注解声明的方式,直接通过手动创建和注册BeanDefinition的方式实现往Spring容器中注入呢?

答案是可以的。

前面说过,BeanDefinition最终会被注册到BeanDefinitionRegistry中,那么如何拿到BeanDefinitionRegistry呢?主要有以下两种方式:

  • ImportBeanDefinitionRegistrar
  • BeanDefinitionRegistryPostProcessor

ImportBeanDefinitionRegistrar

上面在说@Import的时候,关于导入的类实现了ImportBeanDefinitionRegistrar接口的情况没有说,主要是因为在这里说比较合适

public interface ImportBeanDefinitionRegistrar {

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {
       registerBeanDefinitions(importingClassMetadata, registry);
   }

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
   }

}

ImportBeanDefinitionRegistrar中有两个方法,方法的参数就是BeanDefinitionRegistry。当@Import导入的类实现了ImportBeanDefinitionRegistrar接口之后,Spring就会调用registerBeanDefinitions方法,传入BeanDefinitionRegistry。

来个Demo

UserImportBeanDefinitionRegistrar实现ImportBeanDefinitionRegistrar

public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        //构建一个 BeanDefinition , Bean的类型为 User
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
                //设置User这个Bean的属性username的值为三友的java日记
                .addPropertyValue("username", "三友的java日记")
                .getBeanDefinition();
        //把User的BeanDefinition注入到BeanDefinitionRegistry中
        registry.registerBeanDefinition("user", beanDefinition);
    }

}

测试类

@Import(UserImportBeanDefinitionRegistrar.class)
public class UserImportBeanDefinitionRegistrarDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(UserImportBeanDefinitionRegistrarDemo.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

结果

User(username=三友的java日记)

从结果可以看出,成功将User注入到了Spring容器中。

上面的例子中有行代码

applicationContext.register(UserImportBeanDefinitionRegistrarDemo.class);

这行代码的意思就是把UserImportBeanDefinitionRegistrarDemo这个Bean注册到Spring容器中,所以这里其实也算一种将Bean注入到Spring的方式,原理也跟上面一样,会为UserImportBeanDefinitionRegistrarDemo生成一个BeanDefinition注册到Spring容器中。

BeanDefinitionRegistryPostProcessor

除了ImportBeanDefinitionRegistrar可以拿到BeanDefinitionRegistry之外,还可以通过BeanDefinitionRegistryPostProcessor拿到BeanDefinitionRegistry


BeanDefinitionRegistryPostProcessor

这种方式就不演示了。

手动注册BeanDefinition这种方式还是比较常见的。就比如说OpenFeign在启用过程中,会为每个标注了@FeignClient注解的接口创建一个BeanDefinition,然后再往Spring中的注册的,如下是OpenFeign注册FeignClient的部分代码

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        //构建BeanDefinition,class类型为FeignClientFactoryBean
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
        String alias = contextId + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
        //注册BeanDefinition
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
}

注册创建完成的Bean

上一节说可以跳过配置文件或者是注解,直接通过注册BeanDefinition以达到将Bean注入到Spring中的目的。

既然已经可以跳过配置文件或者是注解,那么我们可不可以更激进一步,跳过注册BeanDefinition这一步,直接往Spring中注册一个已经创建好的Bean呢?

答案依然是可以的。

因为上面在提到当创建的Bean是单例的时候,会将这个创建完成的Bean保存到SingletonBeanRegistry中,需要用到直接从SingletonBeanRegistry中查找。既然最终是从SingletonBeanRegistry中查找的Bean,那么直接注入一个创建好的Bean有什么不可以呢?

既然可以,那么如何拿到SingletonBeanRegistry呢?

其实拿到SingletonBeanRegistry的方法其实很多,因为ConfigurableListableBeanFactory就继承了SingletonBeanRegistry接口,所以只要能拿到ConfigurableListableBeanFactory就相当于拿到了SingletonBeanRegistry。


ConfigurableListableBeanFactory类图

而ConfigurableListableBeanFactory可以通过BeanFactoryPostProcessor来获取


BeanFactoryPostProcessor

来个Demo

RegisterUserBeanFactoryPostProcessor实现BeanFactoryPostProcessor,
往Spring容器中添加一个手动创建的User对象

public class RegisterUserBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        //创建一个User对象
        User user = new User();
        user.setUsername("三友的java日记");
        //将这个User对象注入到Spring容器中
        beanFactory.registerSingleton("user", user);
    }

}

测试

public class RegisterUserDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(RegisterUserBeanFactoryPostProcessor.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

结果

User(username=三友的java日记)

从结果还是可以看出,成功从Spring容器中获取到了User对象。

这种直接将创建好的Bean注入到Spring容器中在Spring框架内部使用的还是比较多的,Spring的一些内建的Bean就是通过这个方式注入到Spring中的。

如上图,在SpringBoot项目启动的过程中会往Spring容器中添加两个创建好的Bean,如果你的程序需要使用到这些Bean,就可以通过依赖注入的方式获取到。

虽然基于这种方式可以将Bean注入到Spring容器,但是这种方式注入的Bean是不经过Bean的生命周期的,也就是说这个Bean中诸如@Autowired等注解和Bean生命周期相关的回调都不会生效的,注入到Spring时Bean是什么样就是什么样,Spring不做处理,仅仅只是做一个保存作用。

FactoryBean

FactoryBean是一种特殊的Bean的类型,通过FactoryBean也可以将Bean注入到Spring容器中。


FactoryBean

当我们通过配置文件、注解声明或者是注册BeanDenifition的方式,往Spring容器中注入了一个class类型为FactoryBean类型的Bean时候,其实真正注入的Bean类型为getObjectType方法返回的类型,并且Bean的对象是通过getObject方法返回的。

来个Demo

UserFactoryBean实现了FactoryBean,getObjectType返回了User类型,所以这个UserFactoryBean会往Spring容器中注入User这个Bean,并且User对象是通过getObject()方法的实现返回的。

public class UserFactoryBean implements FactoryBean<User> {
    @Override
    public User getObject() throws Exception {
        User user = new User();
        user.setUsername("三友的java日记");
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

测试

public class UserFactoryBeanDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将UserFactoryBean注入到Spring容器中
        applicationContext.register(UserFactoryBean.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

结果

User(username=三友的java日记)

成功通过UserFactoryBean将User这个Bean注入到Spring容器中了。

FactoryBean这中注入的方式使用也是非常多的,就拿上面举例的OpenFeign来说,OpenFeign为每个FeignClient的接口创建的BeanDefinition的Bean的class类型FeignClientFactoryBean就是FactoryBean的实现。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    
    // FeignClient接口类型
    private Class<?> type;
    
    @Override
    public Object getObject() throws Exception {
       return getTarget();
    }
    
    @Override
    public Class<?> getObjectType() {
       return type;
    }
}

getObject()方法就会返回接口的动态代理的对象,并且这个代理对象是由Feign创建的,这也就实现了Feign和Spring的整合。

总结

通过以上分析可以看出,将Bean注入到Spring容器中大致可以分为5类:

  • 配置文件
  • 注解声明
  • 注册BeanDefinition
  • 注册创建完成的Bean
  • FactoryBean

以上几种注入的方式,在日常业务开发中,基本上都是使用注解声明的方式注入Spring中的;在第三方框架在和Spring整合时,注册BeanDefinition和FactoryBean这些注入方式也会使用的比较多;至于配置文件和注册创建完成的Bean的方式,有但是不多。

最后,本文所有的示例代码地址:

https://github.com/sanyou3/spring-bean-injection.git

往期热门文章推荐

RocketMQ消息短暂而又精彩的一生

写出漂亮代码的45个小技巧

两万字盘点那些被玩烂了的设计模式

RocketMQ保姆级教程

三万字盘点Spring/Boot的那些常用扩展点

@Async注解的坑,小心

扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

扒一扒Bean注入到Spring的那些姿势,你会几种?的更多相关文章

  1. 扒一扒spring,dom4j实现模拟实现读取xml

    今天leadr提出需求,原来公司项目中读取解析xml文件的代码效率太低,考虑切换一种xml为数据封装格式与读取方式以提高效率.我这灵机一动spring对bean的依赖注入就是读取xml文件,可以尝试扒 ...

  2. 【spring set注入 注入集合】 使用set注入的方式注入List集合和Map集合/将一个bean注入另一个Bean

    Dao层代码: package com.it.dao; public interface SayHell { public void sayHello(); } Dao的Impl实现层: packag ...

  3. spring中bean配置和bean注入

    1 bean与spring容器的关系 Bean配置信息定义了Bean的实现及依赖关系,Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载.实例化Bean ...

  4. Spring Bean基本管理--bean注入方式汇总

    依赖注入方式:Spring支持两种依赖注入方式,分别是属性注入和构造函数注入.还有工厂方法注入方式. 依赖注入还分为:注入依赖对象可以采用手工装配或自动装配,在实际应用开发中建议使用手工装配,因为自动 ...

  5. spring可以get到bean,注入却为空

    使用spring的时候,已经将要用的bean注入到容器之中却发现在程序中总是报null,后来发现是因为当前的启动类没有在容器之中,所以用上下文可以get到,但是注入却无效

  6. 【spring】之xml和Annotation,Bean注入的方式

    基于xml形式Bean注入 @Data @AllArgsConstructor @NoArgsConstructor public class PersonBean { private Integer ...

  7. 在Spring的Bean注入中,即使你私有化构造函数,默认他还是会去调用你的私有构造函数去实例化

    在Spring的Bean注入中,即使你私有化构造函数,默认他还是会去调用你的私有构造函数去实例化. 如果我们想保证实例的单一性,就要在定义<bean>时加上factory-method=” ...

  8. Spring Bean 注入 2 注解篇

    1. 自动装配注解 配置applicationContext.xml开启注解 <?xml version="1.0" encoding="UTF-8"?& ...

  9. [spring]Bean注入——在XML中配置

    Bean注入的方式有两种: 一.在XML中配置 属性注入 构造函数注入 工厂方法注入 二.使用注解的方式注入@Autowired,@Resource,@Required 本文首先讲解在XML中配置的注 ...

  10. spring的bean注入扫瞄方法和mybatis的dao bean注入扫描方法

    spring的bean注入扫面方法:@ComponentScan(basePackages = "com.pingan.property.icore.pap.*")mybatis的 ...

随机推荐

  1. 二十三、Pod的service介绍

    Pod 的 Service 介绍 一.Service 介绍 Kubernetes Service 定义了这样一种抽象: 一个 Pod 的逻辑分组,一种可以访问它们的策略,通常称为微服务. 这一组 Po ...

  2. 创建外部表步骤及解决ORA-29913:执行ODCIETTABLEOPEN调出时出错

    创建外部表步骤 建立目录对象(用sys用户创建.授权) 外部表所在路径一定要写对!!! create directory ext_data as 'D:\ORACLE'; grant read,wri ...

  3. Nginx四层负载均衡1

    1.Nginx负载均衡Redis 服务器 IP地址 作用 系统版本 Nginx代理服务器 10.0.0.38 负载均衡服务器 Rocky8.6 Redis服务器1 10.0.0.18 Redis服务器 ...

  4. 基于Camera Link和PCIe DMA的多通道视频采集和显示系统

    基于Camera Link和PCIe DMA的多通道视频采集和显示系统 在主机端PCIe驱动的控制和调度下,视频采集与显示系统可以同时完成对多个Camera Link接口视频采集以及Camera Li ...

  5. java代码整洁之道

    package Day01;import org.junit.Test;import java.text.NumberFormat;import java.util.Scanner;public cl ...

  6. 线上Electron应用具备哪些特征?

    新用户购买<Electron + Vue 3 桌面应用开发>,加小册专属微信群,参与群抽奖,送<深入浅出Electron>.<Electron实战>作者签名版. 1 ...

  7. Seata 1.5.2 源码学习

    文章有点长,我决定用半个小时来给您分享~ 基于Seata 1.5.2,项目中用 seata-spring-boot-starter 1. SeataDataSourceAutoConfiguratio ...

  8. 开发用户K8S授权

    #开发用户没有K8S权限 [ans@master ~]$ kubectl get po Unable to connect to the server: x509: certificate signe ...

  9. 【OpenStack云平台】SecureCRT 连接 CentOS虚拟机

    1.安装SecureCRT SecureCRT是一款支持SSH等协议的终端仿真软件,可以在windows下登录Linux服务器,这样大大方便了开发工作.安装SecureCRT可以通过网上的各种教程安装 ...

  10. EntityUtils MapStruct BeanCopier 数据实体类转换工具 DO BO VO DTO 附视频

    一.序言 在实际项目开发过程中,总有数据实体类互相转换的需求,DO.BO.VO.DTO等数据模型转换经常发生.今天推荐几个好用的实体类转换工具,分别是EntityUtils MapStruct Bea ...