摘要:用法先創建個組件,,,分別在類上加上注解。發現有一個屬性源碼注釋這樣說的自動檢測使用組件。在的方法中,表示不匹配,代表匹配。這就說明使用注冊組件有種方式。
Spring注解應用篇--IOC容器Bean組件注冊
這是Spring注解專題系類文章,本系類文章適合Spring入門者或者原理入門者,小編會在本系類文章下進行企業級應用實戰講解以及spring源碼跟進。環境準備
編譯器IDEA
maven依賴spring-context version:4.3.12.RELEASE
maven依賴junit version:4.11
Bean && Configuration注解小編經歷過xml文件配置的方式,后來使用springboot后發現開箱即用的零xml配置方式(除了框架外中間件等配置~)簡直不要太清爽。然后基于注解驅動開發的特性其實spring早就存在了()
Spring的特性包括IOC和DI(依賴注入)
傳統的xml Bean注入方式:
xml式Bean注入
或者注入Bean的同時進行屬性注入
上面傳統的代碼其實就是等價于:
配置類注冊Bean
@Configuration public class BaseConfig { @Bean("beanIdDefinition") public ExampleBean exampleBean(){ return new ExampleBean("evinhope",666); } }
@ Configuration等價于xml配置文件,表示它是一個配置類,@ bean等價于xml的bean標簽,告訴容器這個bean需要注冊到IOC容器當中。幾乎xml的每一個標簽或者標簽屬性都可以對應一個注解。其中使用bean注解時,默認bean id為方法名(exampleBean),當然也可以通過@ Bean(xxxx)來指定bean的id。
測試用例:
@Test public void shouldAnswerWithTrue() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class);//創建IOC容器 System.out.println("IOC容器創建完成..."); ExampleBean exampleBean = (ExampleBean) ctx.getBean("beanIdDefinition");//獲取id為beanIDDefinition的Bean System.out.println(exampleBean); }
output:
IOC容器創建完成... ExampleBean{name="evinhope", age=666}
創建IOC容器并且獲取后,與getBean相關的方法:
這一些IOC容器方法后續在其他注解證明上可能會用得上,這里挑幾個說明一下:
getBeanNamesForType(bean.class),根據bean的類型返回此類型bean的所有id(ret String[])
getBeanDefinitionNames(),獲取容器中定義的所有bean id(ret String[])
output:
[beanIdDefinition, exampleBean02] [IOC容器在創建過程中往里面注冊的bean, baseConfig, beanIdDefinition, exampleBean02] //其中beanIdDefinition和exampleBean02為同類型的bean,不同id;baseConfig為配置類~
@ Configuration源碼點進去,這個注釋上還有@ Component注釋,說明配置類注釋其實也是一個組件bean
componentScan注解自動掃描組件&指定掃描規則這個注解等價于xml的content:component-scan標簽
componentScan注解包掃描,只要標注了@Controller、@Service、@Repository、@component四大注解的都會自動掃描加入到IOC中。
注解解讀:
注釋源碼點進去后可以看到包含一個@Repeatable注解,跟著點進去,可以得知Repeatable始于JDK1.8,表示其聲明的注釋類型,說明@componentScan可以重復使用,來掃描多個包路徑。
這里關注幾個有意思的注解屬性:
value/basePackages:在xxxx包路徑下掃描組件
includeFilters:指定掃描的時候只包含符合規則的組件(類型聲明為Filter[])
excludeFilters:指定哪些類型不符合組件掃描的條件(類型聲明為Filter[])
在來看Filter的定義信息:
Filter為componentScan注解下的嵌套注解。包含幾個重要的屬性:
FilterType type(默認為FilterType.ANNOTATION):使用過濾的類型
其中FilterType為枚舉類,包含以下值:ANNOTATION(按照注解類型過濾組件)ASSIGNABLE_TYPE(按照主鍵類型過濾組件)ASPECTJ(按照切面表達式)REGEX(按照正則表達式)CUSTOM(自定義)
classes:定義完過濾類型后需要針對過濾類型來解釋過濾的類
pattern:用于過濾器的模式,主要和FilterType為按照切面表達式和按照正則表達式來組合使用。
用法:
先創建3個bean 組件,ControllerBean,ServiceBean,DaoBean(分別在類上加上@Controller、@Service、@Repository注解)。
測試前先用ApplicationContext的getBeanDefinitionNames()方法查看可知ioc中的確不存在上面3個bean組件。
@Configuration @ComponentScan(value= "cn.edu.scau") public class BaseConfig {
使用ApplicationContext的getBeanDefinitionNames()方法打印后,發現3個bean組件已經加進來容器中了,其中,bean id為首字母小寫的類名(controllerBean, daoBean, serviceBean)
進行FilterType的使用。
@ComponentScan(value= "cn.edu.xxx",includeFilters = { @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class}) })
按照上面的說明,此時容器應該只有controller組件,service和dao應該不在容器中,然而事實卻是3種組件都在容器中,這個源碼中說的不一樣???再回過頭看componentScan源碼。
發現有一個屬性boolean useDefaultFilters() default true源碼注釋這樣說的:自動檢測使用@controller@service@component@repository組件。然后上面的代碼再修改一下
@ComponentScan(value= "cn.edu.scau",includeFilters = { @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class}), },useDefaultFilters = false)
再使用getBeanDefinitionNames查看容器bean,發現只剩下了controller注解標注的bean,過濾成功。
由上面說明可知,includeFilter為Filter數組,則可定義多個過濾規則
@ComponentScan(value= "cn.edu.scau",includeFilters = { @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class}), @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE,classes = {ServiceBean.class}) },useDefaultFilters = false)
結果就是容器中新增類型為ServiceBean的組件。
excludeFilters用法同includeFilters一樣,只不過它是過濾掉不符合條件的bean,同時需要搭配userDefaultFilters=false來使用
下面來試試FilterType為自定義的用法:
點進去FilterType源碼后發現CUSTOM上面有注解{@link org.springframework.core.type.filter.TypeFilter} implementation.
這說明自定義規則需要實現TypeFilter接口
再來看看TypeFilter源碼:
接口定義了一個match方法:該方法用于確定包掃描下的類是否匹配
其中帶有2個參數以及返回類型:
@ Param(MetadataReader):當前目標類讀取信息
@ Param (MetadataReaderFactory):這個一個類信息讀取器工廠,可以獲取其他類信息
@ Return(boolean):返回當前類是否符合過濾的要求
public class MyFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return false; } } /* * 同時在配置類中配置 */ @ComponentScan(value= {"cn.edu.scau.controller","cn.edu.scau.service","cn.edu.scau.dao"},includeFilters = { @ComponentScan.Filter(type=FilterType.CUSTOM,classes = {MyFilter.class}) },useDefaultFilters = false)
在測試類中使用ApplicationContext的getBeanDefinitionNames方法發現controller、service、dao三個組件全部不在容器中或者調用Application的getBeanDefinitionCount方法發現比之前的少了3個bean。證明重寫TypeFilter 接口的match方法起作用了,false代表全部不匹配。
源碼點進去看看MetadataReader的屬性描述:
getResource():返回當前類資源引用(類路徑)
getClassMetadata():獲取當前類的類信息
getAnnotationMetadata():獲取當前類的注解信息
//return false的邏輯替換成 Resource resource = metadataReader.getResource(); AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); ClassMetadata classMetadata = metadataReader.getClassMetadata(); String className = classMetadata.getClassName(); if(className.contains("Dao")){ return true; } //只返回類型帶有Dao的類 return false; //其他類一律過濾掉
同理,查看結果,容器中只存在dao的bean組件,另外2個都沒有在容器中出現,完成包掃描的過濾。
當需要多包路徑多掃描規則的時候,可以使用多個componentScan(jdk8 支持,帶有repeatable元注解)或者使用一個componentScans(源碼跟進可知,其實就是一個componentScan數組)
這個注解相當于xml配置文件下bean標簽的scope屬性。
IOC容器的Bean都是單實例,證明測試一下:
/* * 還是上面注冊的那個Bean(id為beanIdDefinition) */ @Test public void shouldAnswerWithTrue() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class); System.out.println("IOC容器創建完成..."); System.out.println(ctx.getBean("beanIdDefinition") == ctx.getBean("beanIdDefinition")); }
結果為true。說明多次從容器中獲取的bean為同一個,即為單實例。
Scope源碼跟進去可以看到屬性value的值可以為singleton、prototype、request、session。分別代表該注解下的bean為單實例(ioc容器啟動后會調用方法創建對象放到容器中,以后需要該對象就從容器中獲取),為多實例(容器創建啟動時不會去調用方法創建對象放進容器中,只有在需要該對象的時候才會去new一個新對象),request代表同一個請求創建一個實例,session同一個session創建一個實例。
同時在bean中增加一個無參構造器
public ExampleBean(){ System.out.println("exampleBean constructor......"); }
測試再跑一次,output:
exampleBean constructor......
IOC容器創建完成...
這說明單實例bean在容器初始化創建的過程中已經注冊了。
在配置類bean中添加@Scope("prototype")
再跑一次,output:
IOC容器創建完成...
exampleBean constructor......
exampleBean constructor......
false
也說明了多實例容器創建啟動時不會去調用方法創建對象放進容器中,只有在需要該對象的時候才會去new一個新對象。
這個注解主要針對單實例bean來說的,上面說過,默認在容器啟動就創建了對象,懶加載ioc啟動后不創建對象,第一次獲取bean的時候再來創建bean,并進行初始化。
在添加懶加載后再測試output:
IOC容器創建完成...
exampleBean constructor......
true
說明對象還是同一個,只是bean的創建容器注冊往后挪了。
代碼跟進去,發現只有一個Condition屬性為一個Class數組。(所有的組件必須匹配才能被注冊)再condition點進去
Condition是一個接口,需要被實現,實現里面的matches方法用來判斷該組件是否條件匹配。
分析到此,思路幾乎清晰,條件匹配類:
public class MyCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //TODO 等一下進行代碼填充 return false; } }
先創建一個Computer的Bean對象,然后在配置類中進行容器注冊
@Bean("window") @Conditional(MyCondition.class) public Computer window(){ return new Computer(); } @Bean("linux") @Conditional(MyCondition.class) public Computer linux(){ return new Computer(); }
測試跑起來后發現,容器中沒有id為linux和window的bean對象。在Conditon的matches方法中,false表示不匹配,ture代表匹配。
現實開發中可能有這樣的需求,不同的環境注冊不同的bean。
因此,嘗試在Condition的matches方法中看看里面的參數代表啥意思.
@ Param ConditionContext:獲取條件上下文環境
@ Param AnnotatedTypeMetadata:注解信息讀取
Environment environment = context.getEnvironment(); String property = environment.getProperty("os.name"); //獲取到bean定義的注冊類.BeanDefinitionRegistry可以用來判斷bean的注冊信息,也可以在容器中注冊bean,后續文章會分析這個類 BeanDefinitionRegistry registry = context.getRegistry(); if(property.contains("windows")){ return false; } return true;
測試跑起來,小編的電腦系統為Windows 10,則2個computer bean全部沒被注冊。
Junit測試可以調整改變JVM的參數,步驟如下:
1、IDE找到Edit Configurations
2、在configuration這里找到VM options,這里可以設置JVM參數。這里我們改變運行的環境,改成linux.。寫法:-Dos.name=linux
測試再跑起來,2個bean又被注冊到容器中了。
可以在測試類獲取容器后再拿到環境確認環境已經改變了
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class); ConfigurableEnvironment environment = ctx.getEnvironment(); String property = environment.getProperty("os.name"); System.out.println(property); System.out.println("IOC容器創建完成...");
ouput:linux
回到Conditional注解的源碼的元注解:@Target({ElementType.TYPE, ElementType.METHOD})。Conditional這個注解可以用于方法和配置類上面,可以延伸如@Conditional注解放在配置上,若不符合條件,那么配置類下的所有bean都不會注冊到IOC容器中。
現實開發場景可以這個判斷條件需要大量使用,在每一個Bean上都寫上@Conditional(MyCondition.class)不太方便和比較繁瑣,因此可以嘗試把他再封裝一層,代碼看起來更加清爽:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(MyCondition.class) public @interface MyDefinitionConditional { }
這樣一來,凡是需要使用@Conditional(MyCondition.class)的地方都可以用@MyDefinitionConditional來代替。
Profile注解環境切換在文檔中是這樣描述這個注解的:@Profile注解事實上是由一個更加靈活的@Conditional注解來實現了。
由源碼切入@Profile,發現此注解上還有@Conditional注解,@Conditional(ProfileCondition.class),ProfileCondition跟進去,發現實現了Condition這個接口(和上面講的@Conditional一樣),下面為源碼中重寫了Condition的matches方法:
@Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (context.getEnvironment() != null) { MultiValueMapattrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } } return true; }
這段代碼先通過上下文環境獲取所有帶有Profile注解的類方法信息,存在Profile注解的話,就會遍歷MultiValueMap字典,判斷一個或者更加多的Profile屬性值是否被當前上下文環境激活。
現實開發中可能會有開發環境、測試環境、線上環境甚至更加多的環境,他們使用的數據源或者一些配置等等都是有差異的,因此它的使用場景也就出來了。
模擬數據源配置幾個Bean:
@Bean("test") @Profile("test") public ExampleBean exampleBeanOfTest(){ return new ExampleBean(); } @Bean("dev") @Profile("dev") public ExampleBean exampleBeanOfDev(){ return new ExampleBean(); } @Bean("prod") @Profile("prod") public ExampleBean exampleBeanOfProd(){ return new ExampleBean(); }
測試:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ConfigurableEnvironment environment = ctx.getEnvironment(); environment.setActiveProfiles("test","dev");//使用代碼配置環境變量 ctx.register(BaseConfig.class); ctx.refresh();
運行后發現測試和開發環境的2個bean已經注冊到容器中了~
或者像上面說的IDE設置JVM參數來達到目的:VM:-Dspring.profiles.active="test","dev"
或者使用@PropertySource加.properties文件同樣可以切換
在resources文件目錄下新建一個application.properties屬性文件,指明環境變量:spring.profiles.active=prod
后再配置類頭上添加注解@PropertySource("classpath:/application.properties")也可達到相同的結果。
總結上面容器注冊bean的方法:1、@Bean注解 2、ComponentScan包掃描+組件標注注解 3、import注解
源碼文檔是這樣說的,import能夠導入一個或更多的bean,也可以通過實現ImportSelector和ImportBeanDefinitionRegistrar接口來進行bean注冊,如果是xml或者其他非bean定義的資源需要被import,可以使用@ImportResource。
這就說明使用import注冊bean組件有3種方式。
直接快速導入
@Configuration @Import(Computer.class) //下面是配置類
測試后發現bean注冊在容器了,bean id為全類名(cn.xxx.xxx.Computer)
實現ImportSelector 接口
public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return null; } }
在配置類中:
@Configuration @Import(MyImportSelector.class) //下面是配置類
測試跑起來,理論上應該是沒有任何bean在容器中注冊的,因為重寫的方法返回null,事實卻報錯了。
::: danger 報錯信息
Failed to process import candidates for configuration class [cn.edu.scau.config.BaseConfig]; nested exception is java.lang.NullPointerException
:::
大致的意思就是空指針異常導致import異常。
源碼跟一下,查看一下方法調用棧后發現異常是由一個叫ConfigurationClassParse類捕獲并且拋出來的,查看try代碼塊,有這樣一段代碼:
for (SourceClass candidate : importCandidates) { if (candidate.isAssignable(ImportSelector.class)) { //省略部分源代碼 String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); CollectionimportSourceClasses = asSourceClasses(importClassNames); } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { //省略此邏輯代碼} else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass)); } }
結合這段代碼不難理解,獲取import注解里面類的信息進行循環遍歷,若是實現ImportSelector接口的是一種情況,實現ImportBeanDefinitionRegistrar的也是另一種情況,剩下的就是把他當作常規import進行處理。我們這里是實現ImportSelector接口屬于第一種情況,調用我們重寫selectImports的方法,我們返回給他null,得到一個名為importClassNames的數組,數組作為asSourceClasses參數,importClassNames.length,為null的對象使用length當然會返回空指針異常,修改一下上面的代碼
public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[0]; } }
selectImports方法參數:
@ Param AnnotationMetadata :標注@Import類(這里為配置類)的類注解信息。
@ Return:返回需要在容器中注冊的bean。bean的id為全類名。如:
return new String[]{"cn.xxx.xxx.bean.Computer"};
實現importBeanDefinitionRegister接口
importBeanDefinitionRegister接口方法參數:
@ Param AnnotationMetadata:同上(注解import這個類的信息)
@ Param BeanDefinitionRegistry:BeanDefinition注冊類,可以使用registerBeanDefinition方法手動注冊進來
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { boolean b = registry.containsBeanDefinition("cn.xxx.xxx.bean.Computer"); if(!b){//沒有Computer這bean就注冊進來 BeanDefinition beanDefinition = new RootBeanDefinition(Computer.class); registry.registerBeanDefinition("computer666",beanDefinition); }else{//存在的話就移除注冊,容器中也會跟著移除 registry.removeBeanDefinition("cn.xxx.xxx.bean.Computer"); } } }
@Configuration @Import({Computer.class,MyImportBeanDefinitionRegistrar.class}) //配置類
運行后Computer這Bean不在容器中,先import了進去后,在MyImportBeanDefinitionRegistrar中又被注冊器給移除了。
FactoryBean注解注冊Bean這是Spring的工廠bean,實現Factory接口,重寫里面的方法
public class ComputerFactoryBean implements FactoryBean{ @Override public Computer getObject() throws Exception { return new Computer(); } @Override public Class> getObjectType() { return Computer.class; } /* * false:代表多實例 true:代表單實例 */ @Override public boolean isSingleton() { return false; } }
`測試:
Object myFactoryBean01 = ctx.getBean("myFactoryBean"); Object myFactoryBean02 = ctx.getBean("myFactoryBean"); System.out.println(myFactoryBean01 == myFactoryBean02); System.out.println(myFactoryBean01.getClass()+" "+myFactoryBean02.getClass());
測試后發現,容器中注冊的是ComputerFactoryBean這個代理工廠bean,然而根據代理工廠的Bean id去容器中取bean對象時又是Computer被代理的bean。那么如何獲取容器中工廠Bean(ComputerFactoryBean)呢。源碼跟一下:
從getBean源碼入手更進去在AbstractBeanFactory這個類中發現:
if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { return beanInstance; }
其中name為getBean(id)我們傳進去的,BeanInstance這個對象由AbstractBeanFactory這個類的doGetBean方法里面調用getSingleton(beanName)這個函數進行獲取,其中beanName由name處理過后的參數,判斷name是否以FACTORY_BEAN_PREFIX(值為&)開頭,不斷循環去掉&頭得到beanName,返回BeanInstance對象(這個對象就是代理工廠bean),進而可以知道想要獲取容器中代理bean通過加&進行處理。
//獲取代理的bean value=Computer Object myFactoryBean01 = ctx.getBean("myFactoryBean"); //getBean前面加上大于等于1的&符號代表獲取FactoryBean value = ComputerFactoryBean Object myFactoryBean02 = ctx.getBean("&&myFactoryBean");
實際中可能會使用工廠Bean來代理某一個Bean,對該對象的所有方法做一個攔截,進行定制化的處理。個人認為倒不如使用基于注解的aspectJ做AOP更加來得方便。
歡迎大家關注一波我的公眾號,嚶嚶嚶(你們的支持是我寫下去的最大動力嗚嗚嗚嗚)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74479.html
摘要:的在單例被破壞時由進行方法調用。定義并實現這兩個接口容器創建完成注解是的縮寫,意思是規范提案。在創建完成并且屬性賦值完成來執行初始化方法在容器銷毀之前回調通知支持自動裝配,類似。 Spring注解應用篇--IOC容器Bean生命周期 這是Spring注解專題系類文章,本系類文章適合Spring入門者或者原理入門者,小編會在本系類文章下進行企業級應用實戰講解以及spring源碼跟進。本文...
摘要:更多相關博文參考前一篇博文講了的方式創建應用,用過的童鞋都知道,早就沒有什么事情了,其實的版本,就已經支持,不用再寫本篇將介紹下,如何利用取代配置本篇博文,建議和上一篇對比看,貼出上一篇地址之基于配置的應用構建構建項目依賴對于依賴 更多spring相關博文參考: http://spring.hhui.top 前一篇博文講了SpringMVC+web.xml的方式創建web應用,用過S...
摘要:挺多人咨詢的,異常處理用切面注解去實現去全局異常處理。全局異常處理類,代碼如下代碼解析如下抽象類是用來處理全局錯誤時進行擴展和實現注解標記的切面排序,值越小擁有越高的優先級,這里設置優先級偏高。 本文內容 為什么要全局異常處理? WebFlux REST 全局異常處理實戰 小結 摘錄:只有不斷培養好習慣,同時不斷打破壞習慣,我們的行為舉止才能夠自始至終都是正確的。 一、為什么要全局...
摘要:響應式編程是基于異步和事件驅動的非阻塞程序,只是垂直通過在內啟動少量線程擴展,而不是水平通過集群擴展。三特性常用的生產的特性如下響應式編程模型適用性內嵌容器組件還有對日志消息測試及擴展等支持。 摘要: 原創出處 https://www.bysocket.com 「公眾號:泥瓦匠BYSocket 」歡迎關注和轉載,保留摘要,謝謝! 02:WebFlux 快速入門實踐 文章工程: JDK...
閱讀 888·2021-11-15 11:38
閱讀 2519·2021-09-08 09:45
閱讀 2819·2021-09-04 16:48
閱讀 2569·2019-08-30 15:54
閱讀 935·2019-08-30 13:57
閱讀 1623·2019-08-29 15:39
閱讀 501·2019-08-29 12:46
閱讀 3525·2019-08-26 13:39