大家好,我是三友~~

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

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

  • 注入办法总结的不全
  • 没有分析能够运用这些注入办法背面的原因
  • 没有这些注入办法在源码中的运用示例

所以本文就带着处理上述的问题的意图来重新整理一下Bean注入到Spring的那些姿态。

微信公众号:三友的java日记

装备文件

装备文件的办法便是以外部化的装备办法来声明Spring Bean,在Spring容器发动时指定装备文件。装备文件办法现在用的不多了,可是为了文章的完整性和连续性,这儿我仍是列出来了,知道的小伙伴能够自行越过这节。

装备文件的类型Spring首要支撑xml和properties两种类型。

xml

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

<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns: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
">
<beanclass="com.sanyou.spring.bean.injection.User"/>
</beans>

User

@Data
@ToString
publicclassUser{
privateStringusername;
}

测验:

publicclassXmlBeanInjectionDemo{
publicstaticvoidmain(String[]args){
ClassPathXmlApplicationContextapplicationContext=newClassPathXmlApplicationContext("classpath:XmlBeanInjectionDemo.xml");
applicationContext.refresh();
Useruser=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

测验:

publicclassPropertiesBeanInjectionDemo{
publicstaticvoidmain(String[]args){
GenericApplicationContextapplicationContext=newGenericApplicationContext();
//创立一个PropertiesBeanDefinitionReader,能够从properties读取Bean的信息,将读到的Bean信息放到applicationContext中
PropertiesBeanDefinitionReaderpropReader=newPropertiesBeanDefinitionReader(applicationContext);
//创立一个properties文件对应的Resource目标
ResourceclassPathResource=newClassPathResource("PropertiesBeanInjectionDemo.properties");
//加载装备文件
propReader.loadBeanDefinitions(classPathResource);
applicationContext.refresh();
Useruser=applicationContext.getBean(User.class);
System.out.println(user);
}
}

成果:

User(username=sanyou)

成功获取到User目标,而且username的特点为properties设置的sanyou。

除了能够装备特点之外还支撑其它的装备,怎么装备能够查看PropertiesBeanDefinitionReader类上的注释。

扒一扒Bean注入到Spring的那些姿势,你会几种?

注解声明

上一节介绍了经过装备文件的办法来声明Bean,可是装备文件这种办法最大的缺陷便是不方便,由于随着项意图不断扩大,可能会发生大量的装备文件。为了处理这个问题,Spring在2.x的版别中开端支撑注解的办法来声明Bean。

@Component + @ComponentScan

这种办法其实就不必多说,在项目中自定义的事务类便是经过@Component及其派生注解(@Service、@Controller等)来注入到Spring容器中的。

在SpringBoot环境底下,一般状况下不需求咱们主动调用@ComponentScan注解,由于@SpringBootApplication会调用@ComponentScan注解,扫描发动引导类(加了@SpringBootApplication注解的类)所在的包及其子包下一切加了@Component注解及其派生注解的类,注入到Spring容器中。

扒一扒Bean注入到Spring的那些姿势,你会几种?

@Bean

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

比方,在运用MybatisPlus的分页插件的时分,就能够按如下办法这么来声明。

@Bean
publicMybatisPlusInterceptormybatisPlusInterceptor(){
MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();
interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));
returninterceptor;
}

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

@Import

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

  • 一般类
  • 类完结了ImportSelector接口
  • 类完结了ImportBeanDefinitionRegistrar接口
一般类

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

类完结了ImportSelector接口
publicinterfaceImportSelector{
String[]selectImports(AnnotationMetadataimportingClassMetadata);
@Nullable
defaultPredicate<String>getExclusionFilter(){
returnnull;
}
}

当@Import导入的类完结了ImportSelector接口的时分,Spring就会调用selectImports办法的完结,获取一批类的全限定名,终究这些类就会被注册到Spring容器中。

比方如下代码中,UserImportSelector完结了ImportSelector,selectImports办法回来User的全限定名

publicclassUserImportSelectorimplementsImportSelector{
@Override
publicString[]selectImports(AnnotationMetadataimportingClassMetadata){
System.out.println("调用UserImportSelector的selectImports办法获取一批类限定名");
returnnewString[]{"com.sanyou.spring.bean.injection.User"};
}
}

当运用@Import注解导入UserImportSelector这个类的时分,其实终究就会把User注入到Spring容器中,如下测验

@Import(UserImportSelector.class)
publicclassImportSelectorDemo{
publicstaticvoidmain(String[]args){
AnnotationConfigApplicationContextapplicationContext=newAnnotationConfigApplicationContext();
//将ImportSelectorDemo注册到容器中
applicationContext.register(ImportSelectorDemo.class);
applicationContext.refresh();
Useruser=applicationContext.getBean(User.class);
System.out.println(user);
}
}

运行成果

User(username=null)

关于类完结了ImportBeanDefinitionRegistrar接口的状况,这个后面说。

一般来说,@Import都是合作@EnableXX这类注解来运用的,比方常见的@EnableScheduling、@EnableAsync注解等,其实终究都是靠@Import来完结的。

扒一扒Bean注入到Spring的那些姿势,你会几种?

@EnableScheduling

扒一扒Bean注入到Spring的那些姿势,你会几种?

@EnableAsync

讲完经过注解的办法来声明Bean之后,能够来思考一个问题,那便是已然注解办法这么简单,为什么Spring还写一堆代码来支撑装备文件这种声明的办法?

其实答案很简单,跟Spring的开展进程有关。Spring在创立之初Java还不支撑注解,所以只能经过装备文件的办法来声明Bean,在Java1.5版别开端支撑注解之后,Spring才开端支撑经过注解的办法来声明Bean。

注册BeanDefinition

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

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

publicinterfaceBeanDefinitionextendsAttributeAccessor,BeanMetadataElement{
//设置BeanclassName
voidsetBeanClassName(@NullableStringbeanClassName);
//获取BeanclassName
@Nullable
StringgetBeanClassName();

//设置是否是懒加载
voidsetLazyInit(booleanlazyInit);
//判断是否是懒加载
booleanisLazyInit();

//判断是否是单例
booleanisSingleton();
}

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

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

扒一扒Bean注入到Spring的那些姿势,你会几种?

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接口的状况没有说,首要是由于在这儿说比较适宜

publicinterfaceImportBeanDefinitionRegistrar{
defaultvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry,BeanNameGeneratorimportBeanNameGenerator){
registerBeanDefinitions(importingClassMetadata,registry);
}
defaultvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
}
}

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

来个Demo

UserImportBeanDefinitionRegistrar完结ImportBeanDefinitionRegistrar

publicclassUserImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{
@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry,BeanNameGeneratorimportBeanNameGenerator){
//构建一个BeanDefinition,Bean的类型为User
AbstractBeanDefinitionbeanDefinition=BeanDefinitionBuilder.rootBeanDefinition(User.class)
//设置User这个Bean的特点username的值为三友的java日记
.addPropertyValue("username","三友的java日记")
.getBeanDefinition();
//把User的BeanDefinition注入到BeanDefinitionRegistry中
registry.registerBeanDefinition("user",beanDefinition);
}
}

测验类

@Import(UserImportBeanDefinitionRegistrar.class)
publicclassUserImportBeanDefinitionRegistrarDemo{
publicstaticvoidmain(String[]args){
AnnotationConfigApplicationContextapplicationContext=newAnnotationConfigApplicationContext();
applicationContext.register(UserImportBeanDefinitionRegistrarDemo.class);
applicationContext.refresh();
Useruser=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

扒一扒Bean注入到Spring的那些姿势,你会几种?

BeanDefinitionRegistryPostProcessor

这种办法就不演示了。

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

classFeignClientsRegistrarimplementsImportBeanDefinitionRegistrar,ResourceLoaderAware,EnvironmentAware{
privatevoidregisterFeignClient(BeanDefinitionRegistryregistry,AnnotationMetadataannotationMetadata,Map<String,Object>attributes){
//构建BeanDefinition,class类型为FeignClientFactoryBean
BeanDefinitionBuilderdefinition=BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
Stringalias=contextId+"FeignClient";
AbstractBeanDefinitionbeanDefinition=definition.getBeanDefinition();
BeanDefinitionHolderholder=newBeanDefinitionHolder(beanDefinition,className,newString[]{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。

扒一扒Bean注入到Spring的那些姿势,你会几种?

ConfigurableListableBeanFactory类图

而ConfigurableListableBeanFactory能够经过BeanFactoryPostProcessor来获取

扒一扒Bean注入到Spring的那些姿势,你会几种?

BeanFactoryPostProcessor

来个Demo

RegisterUserBeanFactoryPostProcessor完结BeanFactoryPostProcessor, 往Spring容器中增加一个手动创立的User目标

publicclassRegisterUserBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{
@Override
publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactorybeanFactory)throwsBeansException{
//创立一个User目标
Useruser=newUser();
user.setUsername("三友的java日记");
//将这个User目标注入到Spring容器中
beanFactory.registerSingleton("user",user);
}
}

测验

publicclassRegisterUserDemo{
publicstaticvoidmain(String[]args){
AnnotationConfigApplicationContextapplicationContext=newAnnotationConfigApplicationContext();
applicationContext.register(RegisterUserBeanFactoryPostProcessor.class);
applicationContext.refresh();
Useruser=applicationContext.getBean(User.class);
System.out.println(user);
}
}

成果

User(username=三友的java日记)

从成果仍是能够看出,成功从Spring容器中获取到了User目标。

这种直接将创立好的Bean注入到Spring容器中在Spring结构内部运用的仍是比较多的,Spring的一些内建的Bean便是经过这个办法注入到Spring中的。

扒一扒Bean注入到Spring的那些姿势,你会几种?

如上图,在SpringBoot项目发动的进程中会往Spring容器中增加两个创立好的Bean,如果你的程序需求运用到这些Bean,就能够经过依靠注入的办法获取到。

尽管根据这种办法能够将Bean注入到Spring容器,可是这种办法注入的Bean是不经过Bean的生命周期的,也便是说这个Bean中比如@Autowired等注解和Bean生命周期相关的回调都不会收效的,注入到Spring时Bean是什么样便是什么样,Spring不做处理,仅仅只是做一个保存效果。

FactoryBean

FactoryBean是一种特别的Bean的类型,经过FactoryBean也能够将Bean注入到Spring容器中。

扒一扒Bean注入到Spring的那些姿势,你会几种?

FactoryBean

当咱们经过装备文件、注解声明或者是注册BeanDenifition的办法,往Spring容器中注入了一个class类型为FactoryBean类型的Bean时分,其实真实注入的Bean类型为getObjectType办法回来的类型,而且Bean的目标是经过getObject办法回来的。

来个Demo

UserFactoryBean完结了FactoryBean,getObjectType回来了User类型,所以这个UserFactoryBean会往Spring容器中注入User这个Bean,而且User目标是经过getObject()办法的完结回来的。

publicclassUserFactoryBeanimplementsFactoryBean<User>{
@Override
publicUsergetObject()throwsException{
Useruser=newUser();
user.setUsername("三友的java日记");
returnuser;
}
@Override
publicClass<?>getObjectType(){
returnUser.class;
}
}

测验

publicclassUserFactoryBeanDemo{
publicstaticvoidmain(String[]args){
AnnotationConfigApplicationContextapplicationContext=newAnnotationConfigApplicationContext();
//将UserFactoryBean注入到Spring容器中
applicationContext.register(UserFactoryBean.class);
applicationContext.refresh();
Useruser=applicationContext.getBean(User.class);
System.out.println(user);
}
}

成果

User(username=三友的java日记)

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

FactoryBean这中注入的办法运用也是十分多的,就拿上面举例的OpenFeign来说,OpenFeign为每个FeignClient的接口创立的BeanDefinition的Bean的class类型FeignClientFactoryBean便是FactoryBean的完结。

classFeignClientFactoryBeanimplementsFactoryBean<Object>,InitializingBean,ApplicationContextAware{

//FeignClient接口类型
privateClass<?>type;

@Override
publicObjectgetObject()throwsException{
returngetTarget();
}

@Override
publicClass<?>getObjectType(){
returntype;
}
}

getObject()办法就会回来接口的动态署理的目标,而且这个署理目标是由Feign创立的,这也就完结了Feign和Spring的整合。

总结

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

  • 装备文件
  • 注解声明
  • 注册BeanDefinition
  • 注册创立完结的Bean
  • FactoryBean

以上几种注入的办法,在日常事务开发中,基本上都是运用注解声明的办法注入Spring中的;在第三方结构在和Spring整合时,注册BeanDefinition和FactoryBean这些注入办法也会运用的比较多;至于装备文件和注册创立完结的Bean的办法,有可是不多。

最终,本文一切的示例代码地址:

github.com/sanyou3/spr…

最终,如果本篇文章对你有点协助,还请帮忙点赞、在看、转发、十分感谢。

查找重视公众号三友的java日记,及时干货不错失,公众号致力于经过画图加上通俗易懂的语言讲解技能,让技能愈加容易学习。

往期热门文章引荐

写出美丽代码的45个小技巧

RocketMQ消息时间短而又精彩的一生

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

RocketMQ保姆级教程

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

@Async注解的坑,小心