大家好,我是三友~~
這篇文章我準備來扒一扒Bean注入到Spring的那些姿勢。
其實關於Bean注入Spring容器的方式網上也有很多相關文章,但是很多文章可能會存在以下常見的問題
所以本文就帶著解決上述的問題的目的來重新梳理一下Bean注入到Spring的那些姿勢。
組態檔的方式就是以外部化的設定方式來宣告Spring Bean,在Spring容器啟動時指定組態檔。組態檔方式現在用的不多了,但是為了文章的完整性和連續性,這裡我還是列出來了,知道的小夥伴可以自行跳過這節。
組態檔的型別Spring主要支援xml和properties兩種型別。
在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。
除了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及其派生註解(@Service、@Controller等)來注入到Spring容器中的。
在SpringBoot環境底下,一般情況下不需要我們主動呼叫@ComponentScan註解,因為@SpringBootApplication會呼叫@ComponentScan註解,掃描啟動引導類(加了@SpringBootApplication註解的類)所在的包及其子包下所有加了@Component註解及其派生註解的類,注入到Spring容器中。
雖然上面@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註解也可以用來將Bean注入到Spring容器中,@Import註解匯入的類可以分為三種情況:
普通類其實就很簡單,就是將@Import匯入的類注入到Spring容器中,這沒什麼好說的。
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是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的一種方式,最終都會轉換成BeanDefinition,Spring是基於BeanDefinition的資訊來建立Bean。
既然Spring最終是基於BeanDefinition的資訊來建立Bean,那麼我們是不是可以跳過組態檔和註解宣告的方式,直接通過手動建立和註冊BeanDefinition的方式實現往Spring容器中注入呢?
答案是可以的。
前面說過,BeanDefinition最終會被註冊到BeanDefinitionRegistry中,那麼如何拿到BeanDefinitionRegistry呢?主要有以下兩種方式:
上面在說@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容器中。
除了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);
}
}
上一節說可以跳過組態檔或者是註解,直接通過註冊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是一種特殊的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類:
以上幾種注入的方式,在日常業務開發中,基本上都是使用註解宣告的方式注入Spring中的;在第三方框架在和Spring整合時,註冊BeanDefinition和FactoryBean這些注入方式也會使用的比較多;至於組態檔和註冊建立完成的Bean的方式,有但是不多。
最後,本文所有的範例程式碼地址:
https://github.com/sanyou3/spring-bean-injection.git
往期熱門文章推薦
掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。