大家好,我是三友~~
這篇文章我準備來扒一扒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注入到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的屬性為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);
}
}
運行結果
對于類實現了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注入到了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);
}
}結果
從結果還是可以看出,成功從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);
}
}結果
成功通過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