扒一扒Bean注入到Spring的那些姿勢
來源:三友的java日記
大家好,我是三友~~
這篇文章我準備來扒一扒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="
xmlns="
xsi:schemaLocation="
/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的方式,有但是不多。
最後,本文所有的示例程式碼地址:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2945713/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 扒一扒安卓的渲染原理安卓
- 扒一扒 HTTP 的構成HTTP
- 扒一扒安卓渲染原理安卓
- 扒一扒ELF檔案
- 扒一扒 EventServiceProvider 原始碼IDE原始碼
- 扒一扒PROMISE的原理,大家不要怕!Promise
- 扒一扒Kotlin協程的底褲Kotlin
- 扒一扒React計算狀態的原理React
- 扒一扒 CSS 語言的誕生史CSS
- 扒一扒程式語言排行榜
- 扒一扒 Jetpack Compose 實現原理Jetpack
- 扒一扒「清華系」的 AI 安防大佬們AI
- 用大資料扒一扒蔡徐坤的真假流量粉大資料
- 扒一扒我們生活中常見的品牌小程式
- 扒一扒「黑客軍團」中用到的黑客工具黑客
- 扒一扒移動網際網路裡的流氓
- 扒一扒隨機數(Random Number)的誕生歷史隨機random
- 扒一扒spring,dom4j實現模擬實現讀取xmlSpringXML
- 從“掃月亮”到“掃福字”,扒一扒背後的支付寶AR框架體系框架
- BEM實戰之扒一扒淘票票頁面
- 防扒
- 扒一扒JVM的垃圾回收機制,下次面試你準備好了嗎JVM面試
- 非得從零開始學習?扒一扒強化學習的致命缺陷強化學習
- 扒一扒9.3閱兵直播如何採用虛擬現實技術
- 日入50000元,扒扒抖音本地生活小程式的變現模式模式
- SpringBoot系列之攔截器注入Bean的幾種姿勢Spring BootBean
- 扒一下Redis的配置檔案Redis
- 釋出防扒提示,
- 看到個美到爆的選單,忍不住扒下來~
- 人剛畢業,顛覆整個AI界:扒一扒Sora兩帶頭人博士論文AISora
- 關於Spring的bean注入SpringBean
- SpringBoot基礎篇Bean之條件注入@Condition使用姿勢Spring BootBean
- spring註解開發(一)Bean注入SpringBean
- Spring注入Bean的幾種方式SpringBean
- 基於node的微小爬蟲——扒了一下知乎爬蟲
- Spring中bean的四種注入方式SpringBean
- spring注入bean的幾種策略模式SpringBean模式
- Spring核心系列之Bean的注入SpringBean