扒一扒Bean注入到Spring的那些姿势,你会几种?
大家好,我是三友~~
这篇文章我准备来扒一扒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来实现的。
讲完通过注解的方式来声明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大致原理图,整个过程大致分为以下几个步骤
通过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
这种方式就不演示了。
手动注册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可以通过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容器中。
当我们通过配置文件、注解声明或者是注册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
往期热门文章推荐
扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。
扒一扒Bean注入到Spring的那些姿势,你会几种?的更多相关文章
- 扒一扒spring,dom4j实现模拟实现读取xml
今天leadr提出需求,原来公司项目中读取解析xml文件的代码效率太低,考虑切换一种xml为数据封装格式与读取方式以提高效率.我这灵机一动spring对bean的依赖注入就是读取xml文件,可以尝试扒 ...
- 【spring set注入 注入集合】 使用set注入的方式注入List集合和Map集合/将一个bean注入另一个Bean
Dao层代码: package com.it.dao; public interface SayHell { public void sayHello(); } Dao的Impl实现层: packag ...
- spring中bean配置和bean注入
1 bean与spring容器的关系 Bean配置信息定义了Bean的实现及依赖关系,Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载.实例化Bean ...
- Spring Bean基本管理--bean注入方式汇总
依赖注入方式:Spring支持两种依赖注入方式,分别是属性注入和构造函数注入.还有工厂方法注入方式. 依赖注入还分为:注入依赖对象可以采用手工装配或自动装配,在实际应用开发中建议使用手工装配,因为自动 ...
- spring可以get到bean,注入却为空
使用spring的时候,已经将要用的bean注入到容器之中却发现在程序中总是报null,后来发现是因为当前的启动类没有在容器之中,所以用上下文可以get到,但是注入却无效
- 【spring】之xml和Annotation,Bean注入的方式
基于xml形式Bean注入 @Data @AllArgsConstructor @NoArgsConstructor public class PersonBean { private Integer ...
- 在Spring的Bean注入中,即使你私有化构造函数,默认他还是会去调用你的私有构造函数去实例化
在Spring的Bean注入中,即使你私有化构造函数,默认他还是会去调用你的私有构造函数去实例化. 如果我们想保证实例的单一性,就要在定义<bean>时加上factory-method=” ...
- Spring Bean 注入 2 注解篇
1. 自动装配注解 配置applicationContext.xml开启注解 <?xml version="1.0" encoding="UTF-8"?& ...
- [spring]Bean注入——在XML中配置
Bean注入的方式有两种: 一.在XML中配置 属性注入 构造函数注入 工厂方法注入 二.使用注解的方式注入@Autowired,@Resource,@Required 本文首先讲解在XML中配置的注 ...
- spring的bean注入扫瞄方法和mybatis的dao bean注入扫描方法
spring的bean注入扫面方法:@ComponentScan(basePackages = "com.pingan.property.icore.pap.*")mybatis的 ...
随机推荐
- 基于FPGA的SATA3.0主机控制器IP
SATA3.0 Host Controller IP SATA3.0 Host IP不仅实现了SATA协议的PHY(物理层).Link(链路层)和TRN(传输层),并且实现了CMD(命令层)和APP( ...
- 【JAVA】详解在JAVA中int与Integer的区别以及背后的原因。
区别 首先我们要明确,这两点之间有什么区别? 主要有以下几点: 数据类型不同:int是基础数据类型,而 Integer是包装数据类型: 默认值不同:int的默认值是 0,而 Integer的默认值是 ...
- 什么是CLR?
CLR翻译为"公共语言运行库",全称是Common Language Runtime,它是一个可以由多种语言使用的"运行时",CLR的核心功能如内存管理,程序集 ...
- 正则表达式之前戏、字符组、量词、特殊符号、贪婪与非贪婪匹配等,python正则模块之re
目录 正则表达式前戏 正则表达式之字符组 正则表达式之特殊符号 正则表达式之量词 贪婪匹配与非贪婪匹配 转义符 正则表达式实战建议 re模块 re模块补充说明 作业 正则表达式前戏 案例:京东注册手机 ...
- 小程序canvas2D绘制印章,话不多说,直接上代码
效果图: CanvasContext 是旧版的接口,不维护了, 新版 Canvas 2D 接口与 Web 一致 官方文档: https://developers.weixin.qq.com/mini ...
- 小米路由器局域网设备ping不通
问题 手机和电脑在同一个局域网内,都连接上小米路由器,我发现电脑部署的服务局域网设备都访问不到,甚至ping不到,排除了防火墙问题,最终发现是路由器一个设置导致的. 解决 将原来的混合加密,更换为强加 ...
- MISC中的图片修改宽高问题
在做CTF中MISC分类题目时,很常见的一个问题就是修改图片正确的宽与高 (此篇笔记中的内容以ctfshow中MISC入门分类为切入点,感兴趣的同学可以一边做一边有不会的看看,仅供参考,我是菜鸡) 曾 ...
- (四) 一文搞懂 JMM - 内存模型
4.JMM - 内存模型 1.JMM内存模型 JMM与happen-before 1.可见性问题产生原因 下图为x86架构下CPU缓存的布局,即在一个CPU 4核下,L1.L2.L3三级缓存与主内存的 ...
- Gepetto:使用chatGPT来对函数功能进行分析并重命名变量的IDA插件
最近OpenAI的chatGPT很火,chatGPT是一个大型的语言模型,能够生成人类语言的文本,主要用于对话式的问答和聊天,以及模拟人类的对话行为 有关chatGPT的介绍就不多赘述了,相关内容很多 ...
- (java 实现开箱即用基于 redis 的分布式锁
项目简介 lock 为 java 设计的分布式锁,开箱即用,纵享丝滑. 开源地址:https://github.com/houbb/lock 目的 开箱即用,支持注解式和过程式调用 基于 redis ...