Spring註解專題系類
小編經歷過xml檔案配置的方式,後來使用springboot後發現開箱即用的零xml配置方式(除了框架外中介軟體等配置~)簡直不要太清爽。然後基於註解驅動開發的特性其實spring早就存在了(:astonished:) Spring的特性包括IOC和DI(依賴注入) 傳統的xml Bean注入方式:
xml
式Bean注入
<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> 複製程式碼
上面傳統的程式碼其實就是等價於: 配置類
註冊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相關的方法:

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陣列)
Scope註解設定元件的作用域
這個註解相當於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一個新物件。
Lazy註解Bean懶載入
這個註解主要針對單例項bean來說的,上面說過,預設在容器啟動就建立了物件,懶載入ioc啟動後不建立物件,第一次獲取bean的時候再來建立bean,並進行初始化。 在新增懶載入後再測試 output:
IOC容器建立完成... exampleBean constructor...... true 說明物件還是同一個,只是bean的建立容器註冊往後挪了。
Conditional註解按照條件來註冊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) { 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")也可達到相同的結果。
import 註解給容器中快速匯入一個元件
總結上面容器註冊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()); 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介面 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<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通過加&進行處理。
//獲取代理的beanvalue=Computer Object myFactoryBean01 = ctx.getBean("myFactoryBean"); //getBean前面加上大於等於1的&符號代表獲取FactoryBeanvalue = ComputerFactoryBean Object myFactoryBean02 = ctx.getBean("&&myFactoryBean"); 複製程式碼
實際中可能會使用工廠Bean來代理某一個Bean,對該物件的所有方法做一個攔截,進行定製化的處理。個人認為倒不如使用基於註解的aspectJ做AOP更加來得方便。
歡迎大家關注一波我的公眾號,嚶嚶嚶(你們的支援是我寫下去的最大動力嗚嗚嗚嗚):
