這是Spring註解專題系類文章,本系類文章適合Spring入門者或者原理入門者,小編會在本系類文章下進行企業級應用實戰講解以及spring源碼跟進。
小編經歷過xml文件配置的方式,後來使用springboot後發現開箱即用的零xml配置方式(除了框架外中間件等配置~)簡直不要太清爽。而後基於註解驅動開發的特性其實spring早就存在了()
Spring的特性包括IOC和DI(依賴注入)
傳統的xml Bean注入方式:java
xml
式Bean注入linux
<bean id="exampleBean" class="xxxx.ExampleBean"/>
或者注入Bean的同時進行屬性注入正則表達式
<bean id="exampleBean" class="xxxx.ExampleBean"> <property name="age" value="666"></property> <property name="name" value="evinhope"></property> </bean>
上面傳統的代碼其實就是等價於:配置類
註冊Beanspring
@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。測試用例
:windows
@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
:springboot
[beanIdDefinition, exampleBean02] [IOC容器在建立過程當中往裏面註冊的bean, baseConfig, beanIdDefinition, exampleBean02] //其中beanIdDefinition和exampleBean02爲同類型的bean,不一樣id;baseConfig爲配置類~
@ Configuration源碼點進去,這個註釋上還有@ Component註釋,說明配置類註釋其實也是一個組件beansession
這個註解等價於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組件。app
@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的參數,步驟以下:
一、IDE找到Edit Configurations
二、在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註解事實上是由一個更加靈活的@Conditional註解來實現了。
由源碼切入@Profile,發現此註解上還有@Conditional註解,@Conditional(ProfileCondition.class),ProfileCondition跟進去,發現實現了Condition這個接口(和上面講的@Conditional同樣),下面爲源碼中重寫了Condition的matches方法:
@Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (context.getEnvironment() != null) { MultiValueMap<String, Object> attrs = 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的方法:一、@Bean註解 二、ComponentScan包掃描+組件標註註解 三、import註解
源碼文檔是這樣說的,import可以導入一個或更多的bean,也能夠經過實現ImportSelector和ImportBeanDefinitionRegistrar接口來進行bean註冊,若是是xml或者其餘非bean定義的資源須要被import,可使用@ImportResource。
這就說明使用import註冊bean組件有3種方式。
@Configuration @Import(Computer.class) //下面是配置類
測試後發現bean註冊在容器了,bean id爲全類名(cn.xxx.xxx.Computer)
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()); Collection<SourceClass> importSourceClasses = 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接口方法參數:
@ 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中又被註冊器給移除了。
這是Spring的工廠bean,實現Factory接口,重寫裏面的方法
public class ComputerFactoryBean implements FactoryBean<Computer>{ @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更加來得方便。
歡迎你們關注一波個人公衆號,嚶嚶嚶(大家的支持是我寫下去的最大動力嗚嗚嗚嗚)