首先,我們將探討一些Spring框架中IOC(Inversion of Control)的高級特性,特別是組件掃描的相關(guān)知識。組件掃描是Spring框架中一個(gè)重要的特性,它可以自動(dòng)檢測并實(shí)例化帶有特定注解(如@Component,@Service,@Controller等)的類,并將它們注冊為Spring上下文中的bean。這里,我們會通過一些詳細(xì)的例子來闡明這些概念,并且展示如何在實(shí)際的代碼中使用這些特性。
1. 組件掃描路徑
@ComponentScan注解是用于指定Spring在啟動(dòng)時(shí)需要掃描的包路徑,從而自動(dòng)發(fā)現(xiàn)并注冊組件。 我們設(shè)置組件掃描路徑包括兩種方式:
直接指定包名:如@ComponentScan("com.example.demo"),等同于@ComponentScan(basePackages = {"com.example.demo"}),Spring會掃描指定包下的所有類,并查找其中帶有@Component、@Service、@Repository等注解的組件,然后將這些組件注冊為Spring容器的bean。
指定包含特定類的包:如@ComponentScan(basePackageClasses = {ExampleService.class}),Spring會掃描ExampleService類所在的包以及其所有子包。
接下來,給出了一個(gè)完整的例子,說明如何使用第二種方式來設(shè)置組件掃描路徑。這可以通過設(shè)置@ComponentScan的basePackageClasses屬性來實(shí)現(xiàn)。例如:
@Configuration @ComponentScan(basePackageClasses = {ExampleService.class}) public class BasePackageClassConfiguration { }以上代碼表示,Spring會掃描ExampleService類所在的包以及其所有子包。 全部代碼如下: 首先,我們創(chuàng)建一個(gè)名為ExampleService的服務(wù)類
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class ExampleService { }接著在bean目錄下創(chuàng)建DemoDao
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class DemoDao { }然后在配置類中設(shè)置組件掃描路徑
package com.example.demo.configuration; import com.example.demo.service.ExampleService; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = ExampleService.class) public class BasePackageClassConfiguration { }我們還會創(chuàng)建一個(gè)名為DemoApplication的類,這個(gè)類的作用是啟動(dòng)Spring上下文并打印所有注冊的bean的名稱。
package com.example.demo; import com.example.demo.configuration.BasePackageClassConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.util.Arrays; public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BasePackageClassConfiguration.class); String[] beanDefinitionNames = ctx.getBeanDefinitionNames(); Arrays.stream(beanDefinitionNames).forEach(System.out::println); } }運(yùn)行上述DemoApplication類的main方法,就會在控制臺上看到所有注冊的bean的名稱,包括我們剛剛創(chuàng)建的ExampleService。
現(xiàn)在,如果我們在ExampleService類所在的包或者其任意子包下創(chuàng)建一個(gè)新的類(例如TestService),那么這個(gè)組件類也會被自動(dòng)注冊為一個(gè)bean。這就是basePackageClasses屬性的作用:它告訴Spring要掃描哪些包以及其子包。
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class TestService { }如果再次運(yùn)行DemoApplication類的main方法,就會看到TestService也被打印出來,說明它也被成功注冊為了一個(gè)bean。
我們可以看到這個(gè)DemoDao始終沒有被掃描到,我們看一下@ComponentScan注解的源碼
可以看到basePackageClasses屬性這是一個(gè)數(shù)組類型的,有人會疑問了,剛剛我們寫的@ComponentScan(basePackageClasses = ExampleService.class),這沒有用到數(shù)組啊,為什么這里還能正常運(yùn)行呢? 如果數(shù)組只包含一個(gè)元素,可以在賦值時(shí)省略數(shù)組的大括號{},這是Java的一種語法糖。在這種情況下,編譯器會自動(dòng)把該元素包裝成一個(gè)數(shù)組。 例如,以下兩種寫法是等價(jià)的:
@ComponentScan(basePackageClasses = {ExampleService.class})
和
@ComponentScan(basePackageClasses = ExampleService.class)在上面兩種情況下,ExampleService.class都會被包裝成一個(gè)只包含一個(gè)元素的數(shù)組。這是Java語法的一個(gè)便利特性,使得代碼在只有一個(gè)元素的情況下看起來更加簡潔。 那么為了DemoDao組件被掃描到,我們可以在basePackageClasses屬性加上DemoDao類,這樣就可以掃描DemoDao組件所在的包以及它的子包。
package com.example.demo.configuration; import com.example.demo.bean.DemoDao; import com.example.demo.service.ExampleService; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = {ExampleService.class, DemoDao.class}) public class BasePackageClassConfiguration { }運(yùn)行結(jié)果
@ComponentScan注解的源碼還有不少,后面我們用到再說
2. 按注解過濾組件(包含)
除了基本的包路徑掃描,Spring還提供了過濾功能,允許我們通過設(shè)定過濾規(guī)則,只包含或排除帶有特定注解的類。
這個(gè)過濾功能對于大型項(xiàng)目中的模塊劃分非常有用,可以精細(xì)控制Spring的組件掃描范圍,優(yōu)化項(xiàng)目啟動(dòng)速度。 在Spring中可以通過@ComponentScan的includeFilters屬性來實(shí)現(xiàn)注解包含過濾,只包含帶有特定注解的類。
在下面這個(gè)例子中,我們將創(chuàng)建一些帶有特定注解的組件,并設(shè)置一個(gè)配置類來掃描它們。 全部代碼如下: 創(chuàng)建一個(gè)新的注解Species:
package com.example.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Species { }接下來,我們將創(chuàng)建三個(gè)不同的組件,其中兩個(gè)包含Species注解:
package com.example.demo.bean; import com.example.demo.annotation.Species; import org.springframework.stereotype.Component; @Species public class Elephant { }Elephant類被@Species修飾,沒有@Component修飾。
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Monkey { }Monkey只被@Component修飾
package com.example.demo.bean; import com.example.demo.annotation.Species; import org.springframework.stereotype.Component; @Component @Species public class Tiger { }如上所示,Tiger有@Component和@Species修飾。 接著,我們需要?jiǎng)?chuàng)建一個(gè)配置類,用于掃描和包含有Species注解的組件:
package com.example.demo.configuration; import com.example.demo.annotation.Species; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(basePackages = "com.example.demo", includeFilters = @Filter(type = FilterType.ANNOTATION, classes = Species.class), useDefaultFilters = false) public class FilterConfiguration { }在這個(gè)配置類中,我們設(shè)置了@ComponentScan注解的includeFilters屬性,F(xiàn)ilterType.ANNOTATION代表按注解過濾,這里用于掃描包含所有帶有Species注解的組件。注意,useDefaultFilters = false表示禁用了默認(rèn)的過濾規(guī)則,只會包含標(biāo)記為Species的組件。
有人可能會疑問了,useDefaultFilters = false表示禁用了默認(rèn)的過濾規(guī)則,什么是默認(rèn)的過濾規(guī)則? 在Spring中,當(dāng)使用@ComponentScan注解進(jìn)行組件掃描時(shí),Spring提供了默認(rèn)的過濾規(guī)則。這些默認(rèn)規(guī)則包括以下幾種類型的注解:
@Component
@Repository
@Service
@Controller
@RestController
@Configuration
默認(rèn)不寫useDefaultFilters屬性的情況下,useDefaultFilters屬性的值為true,Spring在進(jìn)行組件掃描時(shí)會默認(rèn)包含以上注解標(biāo)記的組件,如果將useDefaultFilters設(shè)置為false,Spring就只會掃描明確指定過濾規(guī)則的組件,不再包括以上默認(rèn)規(guī)則的組件。
所以,useDefaultFilters = false是在告訴Spring我們只想要自定義組件掃描規(guī)則。 最后,我們創(chuàng)建一個(gè)主程序,來實(shí)例化應(yīng)用上下文并列出所有的Bean名稱:
package com.example.demo; import com.example.demo.configuration.FilterConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(beanName); } } }當(dāng)我們運(yùn)行這個(gè)程序時(shí),會看到輸出的Bean名字只包括被Species注解標(biāo)記的類。在這個(gè)例子中會看到Tiger和Elephant,但不會看到Monkey,因?yàn)槲覀兊呐渲弥话吮籗pecies注解的類。 運(yùn)行結(jié)果:
如果useDefaultFilters屬性設(shè)置為true,那么運(yùn)行程序時(shí)輸出的Bean名字將會包括Monkey。 總結(jié):上面介紹了如何使用Spring的@ComponentScan注解中的includeFilters屬性和useDefaultFilters屬性來過濾并掃描帶有特定注解的類。
通過自定義注解和在配置類中設(shè)置相關(guān)屬性,可以精確地控制哪些類被Spring容器掃描和管理。如果設(shè)置useDefaultFilters為false,則Spring只掃描被明確指定過濾規(guī)則的組件,不再包含默認(rèn)規(guī)則(如@Component、@Service等)的組件。
3. 按注解過濾組件(排除)
在Spring框架中,我們不僅可以通過@ComponentScan注解的includeFilters屬性設(shè)置包含特定注解的類,還可以通過excludeFilters屬性來排除帶有特定注解的類。這個(gè)功能對于我們自定義模塊的加載非常有用,我們可以通過這種方式,精確控制哪些組件被加載到Spring的IOC容器中。
下面我們將通過一個(gè)具體的示例來說明如何使用@ComponentScan的excludeFilters屬性來排除帶有特定注解的類。 全部代碼如下: 首先,創(chuàng)建一個(gè)注解Exclude:
package com.example.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Exclude { }定義三個(gè)類Elephant、Monkey、Tiger
package com.example.demo.bean; import com.example.demo.annotation.Exclude; import org.springframework.stereotype.Component; @Component @Exclude public class Elephant { }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Monkey { }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Tiger { }注意,這幾個(gè)類都標(biāo)記為@Component,Elephant類上有@Exclude注解。 接下來,我們創(chuàng)建配置類FilterConfiguration,在其中使用@ComponentScan注解,并通過excludeFilters屬性排除所有標(biāo)記為@Exclude的類:
package com.example.demo.configuration; import com.example.demo.annotation.Exclude; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(basePackages = "com.example.demo", excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Exclude.class)) public class FilterConfiguration { }這樣,在Spring IOC容器中,只有Tiger和Monkey類會被掃描并實(shí)例化,因?yàn)镋lephant類被@Exclude注解標(biāo)記,而我們在FilterConfiguration類中排除了所有被@Exclude注解標(biāo)記的類。 主程序?yàn)椋?
package com.example.demo; import com.example.demo.configuration.FilterConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(beanName); } } }運(yùn)行結(jié)果:
總結(jié):這小節(jié)主要講解了如何在Spring框架中通過@ComponentScan注解的excludeFilters屬性進(jìn)行注解過濾,以精確控制加載到Spring IOC容器中的組件。在本小節(jié)的示例中,我們首先創(chuàng)建了一個(gè)名為Exclude的注解,然后定義了三個(gè)類Elephant、Monkey、和Tiger,它們都被標(biāo)記為@Component,其中Elephant類上還有一個(gè)@Exclude注解。
接下來,我們創(chuàng)建了一個(gè)配置類FilterConfiguration,其中使用了@ComponentScan注解,并通過excludeFilters屬性排除了所有標(biāo)記為@Exclude的類。因此,當(dāng)程序運(yùn)行時(shí),Spring IOC容器中只有Tiger和Monkey類會被掃描并實(shí)例化,因?yàn)镋lephant類被@Exclude注解標(biāo)記,所以被排除了。
4. 通過正則表達(dá)式過濾組件
在Spring框架中,除了可以通過指定注解來進(jìn)行包含和排除類的加載,我們還可以利用正則表達(dá)式來對組件進(jìn)行過濾。這種方式提供了一種更靈活的方式來選擇需要被Spring IOC容器管理的類。具體來說,可以利用正則表達(dá)式來包含或者排除名稱符合某個(gè)特定模式的類。
下面,我們將通過一個(gè)具體的例子來展示如何使用正則表達(dá)式過濾來只包含類名以特定字符串結(jié)尾的類。 下面的例子將演示如何只包含類名以Tiger結(jié)尾的類。 全部代碼如下: 定義三個(gè)類Tiger、Elephant、Monkey
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Elephant { }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Monkey { }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Tiger { }接著我們創(chuàng)建配置類FilterConfiguration,使用@ComponentScan注解,并通過includeFilters屬性來包含類名以Tiger結(jié)尾的類:
package com.example.demo.configuration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(basePackages = "com.example.demo", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Tiger")) public class FilterConfiguration { }在上述示例中,我們使用FilterType.REGEX過濾類型,并指定要包含的正則表達(dá)式模式".*Tiger"。結(jié)果只會有Tiger類會被Spring的IOC容器掃描并實(shí)例化,因?yàn)橹挥蠺iger類的類名滿足正則表達(dá)式".*Tiger"。這里.代表任意單個(gè)字符 (除了換行符),*代表前一個(gè)字符重復(fù)任意次,.*組合起來表示匹配任意個(gè)任意字符。 主程序:
package com.example.demo; import com.example.demo.configuration.FilterConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(beanName); } } }運(yùn)行結(jié)果
總結(jié):本小節(jié)介紹了如何在Spring框架中使用正則表達(dá)式對組件進(jìn)行過濾,以選擇哪些類應(yīng)被Spring IOC容器管理。在所給示例中,首先定義了三個(gè)類Elephant、Monkey和Tiger。然后創(chuàng)建了一個(gè)配置類FilterConfiguration,使用了@ComponentScan注解,并通過includeFilters屬性設(shè)置了一個(gè)正則表達(dá)式" .*Tiger",用于選擇類名以"Tiger"結(jié)尾的類。所以在運(yùn)行主程序時(shí),Spring的IOC容器只會掃描并實(shí)例化Tiger類,因?yàn)橹挥蠺iger類的類名滿足正則表達(dá)式" .*Tiger"。
5. Assignable 類型過濾組件
"Assignable類型過濾 " 是Spring框架在進(jìn)行組件掃描時(shí)的一種過濾策略,該策略允許我們指定一個(gè)或多個(gè)類或接口,然后Spring會包含或排除所有可以賦值給這些類或接口的類。如果我們指定了一個(gè)特定的接口,那么所有實(shí)現(xiàn)了這個(gè)接口的類都會被包含(或者排除)。同樣,如果指定了一個(gè)具體的類,那么所有繼承自這個(gè)類的類也會被包含(或者排除)。 在下面的例子中,我們將使用 “Assignable類型過濾” 來包含所有實(shí)現(xiàn)了Animal接口的類。 全部代碼如下: 首先,我們定義一個(gè)Animal接口
package com.example.demo.bean; public interface Animal { }接著定義三個(gè)類:Elephant、Monkey和Tiger,其中Tiger沒有實(shí)現(xiàn)Animal接口
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Elephant implements Animal { }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Monkey implements Animal { }
package com.example.demo.bean import org.springframework.stereotype.Component; @Component public class Tiger { }然后,我們創(chuàng)建一個(gè)FilterConfiguration類并使用@ComponentScan來掃描所有實(shí)現(xiàn)了Animal接口的類。
package com.example.demo.configuration; import com.example.demo.bean.Animal; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(basePackages = "com.example.demo", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Animal.class)) public class FilterConfiguration { }這種過濾方式在@ComponentScan注解中通過FilterType.ASSIGNABLE_TYPE來使用,這里Spring將只掃描并管理所有實(shí)現(xiàn)了Animal接口的類。 最后,我們創(chuàng)建一個(gè)主程序來測試:
package com.example.demo; import com.example.demo.configuration.FilterConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(beanName); } } }運(yùn)行結(jié)果:
這里也可以看到,只有實(shí)現(xiàn)了Animal接口的類才會被Spring的IoC容器掃描并實(shí)例化,其他的@Component類沒有實(shí)現(xiàn)Animal接口的bean將不會被掃描和實(shí)例化。 總結(jié):本小節(jié)介紹了Spring框架中的 "Assignable類型過濾 ",這是一種可以指定一個(gè)或多個(gè)類或接口進(jìn)行組件掃描的過濾策略。
Spring會包含或排除所有可以賦值給這些類或接口的類。在本小節(jié)的例子中,首先定義了一個(gè)Animal接口,然后定義了三個(gè)類Elephant、Monkey和Tiger,其中Elephant和Monkey實(shí)現(xiàn)了Animal接口,而Tiger沒有。然后創(chuàng)建了一個(gè)FilterConfiguration類,使用了@ComponentScan注解,并通過includeFilters屬性和FilterType.ASSIGNABLE_TYPE類型來指定掃描所有實(shí)現(xiàn)了Animal接口的類。
因此,當(dāng)運(yùn)行主程序時(shí),Spring的IOC容器只會掃描并實(shí)例化實(shí)現(xiàn)了Animal接口的Elephant和Monkey類,未實(shí)現(xiàn)Animal接口的Tiger類不會被掃描和實(shí)例化。
6. 自定義組件過濾器
Spring也允許我們定義自己的過濾器來決定哪些組件將被Spring IoC容器掃描。為此,我們需要實(shí)現(xiàn)TypeFilter接口,并重寫match()方法。在match()方法中,我們可以自定義選擇哪些組件需要被包含或者排除。 全部代碼如下: 新增一個(gè)接口Animal
package com.example.demo.bean; public interface Animal { String getType(); }定義幾個(gè)類,實(shí)現(xiàn)Animal接口
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Elephant implements Animal { public String getType() { return "This is a Elephant."; } }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Monkey implements Animal { public String getType() { return "This is an Monkey."; } }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Tiger implements Animal { public String getType() { return "This is a Tiger."; } }
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Tiger2 { public String getType() { return "This is a Tiger2."; } }Tiger2沒實(shí)現(xiàn)Animal接口,后面用來對比。 下面我們先個(gè)自定義一個(gè)過濾器CustomFilter,它實(shí)現(xiàn)了TypeFilter接口,這個(gè)過濾器會包含所有實(shí)現(xiàn)了Animal接口并且類名以"T"開頭的類:
package com.example.demo.filter; import com.example.demo.bean.Animal; import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; import java.util.Arrays; public class CustomFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { ClassMetadata classMetadata = metadataReader.getClassMetadata(); // 如果全限定類名以 "T" 開頭并且實(shí)現(xiàn)了 "Animal" 接口 return classMetadata.getClassName().startsWith("com.example.demo.bean.T") && Arrays.asList(classMetadata.getInterfaceNames()).contains(Animal.class.getName()); } }如果match方法返回true,那么Spring將把這個(gè)類視為候選組件,還需滿足其他條件才能創(chuàng)建bean,如果這個(gè)類沒有使用@Component、@Service等注解,那么即使過濾器找到了這個(gè)類,Spring也不會將其注冊為bean。因?yàn)镾pring依然需要識別類的元數(shù)據(jù)(如:@Component、@Service等注解)來確定如何創(chuàng)建和管理bean。反之,如果match方法返回false,那么Spring將忽略這個(gè)類。 在match方法中
metadataReader.getClassMetadata()返回一個(gè)ClassMetadata對象,它包含了關(guān)于當(dāng)前類的一些元數(shù)據(jù)信息,例如類名、是否是一個(gè)接口、父類名等。
classMetadata.getClassName()返回當(dāng)前類的全限定類名,也就是包括了包名的類名。
在match方法中,我們檢查了當(dāng)前類的全限定名是否以"com.example.demo.bean.T"開頭,并且當(dāng)前類是否實(shí)現(xiàn)了"Animal"接口。如果滿足這兩個(gè)條件,match方法就返回true,Spring會將這個(gè)類視為候選組件。如果這兩個(gè)條件有任何一個(gè)不滿足,match方法就返回false,Spring就會忽略這個(gè)類,不會將其視為候選組件。 然后,在我們的FilterConfiguration中,使用FilterType.CUSTOM類型,并且指定我們剛才創(chuàng)建的CustomFilter類:
package com.example.demo.configuration; import com.example.demo.filter.CustomFilter; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration @ComponentScan(basePackages = "com.example.demo", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, classes = CustomFilter.class)) public class FilterConfiguration { }這樣,當(dāng)Spring IoC容器進(jìn)行掃描的時(shí)候,只有類名以"T"開頭并且實(shí)現(xiàn)了Animal接口的組件才會被包含。在我們的例子中,只有Tiger類會被包含,Tiger2、Elephant和Monkey類將被排除。 主程序:
package com.example.demo; import com.example.demo.configuration.FilterConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(beanName); } } }運(yùn)行結(jié)果:
調(diào)試會發(fā)現(xiàn),match方法在不停的回調(diào)。其實(shí)match方法的調(diào)用次數(shù)和Spring應(yīng)用上下文中的Bean定義數(shù)量是相關(guān)的,當(dāng)我們使用@ComponentScan進(jìn)行包掃描時(shí),Spring會遍歷指定包(及其子包)下的所有類,對每個(gè)類進(jìn)行分析以決定是否需要?jiǎng)?chuàng)建對應(yīng)的Bean。
當(dāng)我們使用@ComponentScan.Filter定義自定義的過濾器時(shí),Spring會為每個(gè)遍歷到的類調(diào)用過濾器的match方法,以決定是否需要忽略這個(gè)類。因此,match方法被調(diào)用的次數(shù)等于Spring掃描到的類的數(shù)量,不僅包括最終被創(chuàng)建為Bean的類,也包括被過濾器忽略的類。
這個(gè)行為可能受到一些其他配置的影響。例如,如果Spring配置中使用了懶加載 (@Lazy),那么match方法的調(diào)用可能會被延遲到Bean首次被請求時(shí)。 總結(jié):本小節(jié)介紹了如何在Spring框架中創(chuàng)建和使用自定義過濾器,以決定哪些組件將被Spring IoC容器視為候選組件。
通過實(shí)現(xiàn)TypeFilter接口并重寫其match()方法,可以根據(jù)自定義的條件決定哪些類會被包含在候選組件的列表中。在這個(gè)例子中,我們創(chuàng)建了一個(gè)自定義過濾器,只有以"T"開頭且實(shí)現(xiàn)了Animal接口的類才會被標(biāo)記為候選組件。當(dāng)Spring進(jìn)行包掃描時(shí),會遍歷所有的類,并對每個(gè)類調(diào)用過濾器的match()方法,這個(gè)方法的調(diào)用次數(shù)等于Spring掃描到的類的數(shù)量。
然后,只有那些同時(shí)滿足過濾器條件并且被Spring識別為組件的類(例如,使用了@Component或@Service等注解),才會被實(shí)例化為Bean并被Spring IoC容器管理。如果配置了懶加載,那么Bean的實(shí)例化可能會被延遲到Bean首次被請求時(shí)。
7. 組件掃描的其他特性
Spring的組件掃描機(jī)制提供了一些強(qiáng)大的特性,我們來逐一講解。
7.1 組合使用組件掃描
Spring提供了@ComponentScans注解,讓我們能夠組合多個(gè)@ComponentScan使用,這樣可以讓我們在一次操作中完成多次包掃描。 @ComponentScans的主要使用場景是當(dāng)需要對Spring的組件掃描行為進(jìn)行更精細(xì)的控制時(shí),可以在同一個(gè)應(yīng)用程序中掃描兩個(gè)完全獨(dú)立的包,也可以在應(yīng)用多個(gè)獨(dú)立的過濾器來排除或包含特定的組件。
可以看到@ComponentScans注解接收了一個(gè)ComponentScan數(shù)組,也就是一次性組合了一堆@ComponentScan注解。 讓我們通過一個(gè)例子來看看如何使用@ComponentScans來組合多個(gè)@ComponentScan。 全部代碼如下: 首先,我們定義兩個(gè)簡單的類,分別在com.example.demo.bean1和com.example.demo.bean2包中:
package com.example.demo.bean1; import org.springframework.stereotype.Component; @Component public class BeanA { }
package com.example.demo.bean2; import org.springframework.stereotype.Component; @Component public class BeanB { }然后,我們在配置類中使用@ComponentScans來一次性掃描這兩個(gè)包:
package com.example.demo.configuration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScans; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScans({ @ComponentScan("com.example.demo.bean1"), @ComponentScan("com.example.demo.bean2") }) public class AppConfig { }
最后,我們可以測試一下是否成功地掃描到了這兩個(gè)類:
package com.example.demo; import com.example.demo.bean1.BeanA; import com.example.demo.bean2.BeanB; import com.example.demo.configuration.AppConfig; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); BeanA beanA = ctx.getBean(BeanA.class); BeanB beanB = ctx.getBean(BeanB.class); System.out.println("beanA = " + beanA); System.out.println("beanB = " + beanB); } }運(yùn)行上述main方法,BeanA和BeanB就成功地被掃描并注入到了Spring的ApplicationContext中。 運(yùn)行結(jié)果:
總結(jié):本小節(jié)介紹了Spring包掃描機(jī)制的一個(gè)重要特性,即能夠使用@ComponentScans注解進(jìn)行組合包掃描。這個(gè)特性允許在一次操作中完成多次包掃描,實(shí)現(xiàn)對Spring組件掃描行為的精細(xì)控制。例如,可以同時(shí)掃描兩個(gè)完全獨(dú)立的包,或者應(yīng)用多個(gè)獨(dú)立的過濾器來排除或包含特定的組件。在本小節(jié)的示例中,使用@ComponentScans一次性掃描了com.example.demo.bean1和com.example.demo.bean2兩個(gè)包,成功地將BeanA和BeanB掃描并注入到Spring的ApplicationContext中。
8. 組件掃描的組件名稱生成
當(dāng)我們在Spring中使用注解進(jìn)行bean的定義和管理時(shí),通常會用到@Component,@Service,@Repository,@Controller等注解。在使用這些注解進(jìn)行bean定義的時(shí)候,如果我們沒有明確指定bean的名字,那么Spring會根據(jù)一定的規(guī)則為我們的bean生成一個(gè)默認(rèn)的名字。 這個(gè)默認(rèn)的名字一般是類名的首字母小寫。例如,對于一個(gè)類名為MyService的類,如果我們像這樣使用@Service注解:
@Service public class MyService { }那么Spring會為我們的bean生成一個(gè)默認(rèn)的名字myService。我們可以在應(yīng)用的其他地方通過這個(gè)名字來引用這個(gè)bean。例如,我們可以在其他的bean中通過@Autowired注解和這個(gè)名字來注入這個(gè)bean:
@Autowired private MyService myService;這個(gè)默認(rèn)的名字是通過BeanNameGenerator接口的實(shí)現(xiàn)類AnnotationBeanNameGenerator來生成的。AnnotationBeanNameGenerator會檢查我們的類是否有明確的指定了bean的名字,如果沒有,那么它就會按照類名首字母小寫的規(guī)則來生成一個(gè)默認(rèn)的名字。
8.1 Spring 是如何生成默認(rèn) bean 名稱的(源碼分析)
為了解釋這個(gè)過程,讓我們看一下AnnotationBeanNameGenerator類的源碼,以下源碼對應(yīng)的Spring版本是5.3.7。 先給出源碼圖片,后面給出源碼分析
代碼塊提出來分析:
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { if (definition instanceof AnnotatedBeanDefinition) { // 該行檢查BeanDefinition是否為AnnotatedBeanDefinition String beanName = this.determineBeanNameFromAnnotation((AnnotatedBeanDefinition)definition); // 該行調(diào)用方法來從注解獲取bean名稱 if (StringUtils.hasText(beanName)) { // 檢查是否獲取到了有效的bean名稱 return beanName; // 如果有,返回這個(gè)名稱 } } return this.buildDefaultBeanName(definition, registry); // 如果沒有從注解中獲取到有效的名稱,調(diào)用方法生成默認(rèn)的bean名稱 }再看看determineBeanNameFromAnnotation方法
這段代碼很長,我們直接將代碼塊提出來分析:
@Nullable protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { // 1. 獲取bean定義的元數(shù)據(jù),包括所有注解信息 AnnotationMetadata amd = annotatedDef.getMetadata(); // 2. 獲取所有注解類型 Set最后看看buildDefaultBeanName方法,Spring是如何生成bean的默認(rèn)名稱的。types = amd.getAnnotationTypes(); // 3. 初始化bean名稱為null String beanName = null; // 4. 遍歷所有注解類型 Iterator var5 = types.iterator(); while(var5.hasNext()) { // 4.1 獲取當(dāng)前注解類型 String type = (String)var5.next(); // 4.2 獲取當(dāng)前注解的所有屬性 AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type); // 4.3 只有當(dāng)前注解的屬性不為null時(shí),才會執(zhí)行以下代碼 if (attributes != null) { Set metaTypes = (Set)this.metaAnnotationTypesCache.computeIfAbsent(type, (key) -> { Set result = amd.getMetaAnnotationTypes(key); return result.isEmpty() ? Collections.emptySet() : result; }); // 4.4 檢查當(dāng)前注解是否為帶有名稱的元注解 if (this.isStereotypeWithNameValue(type, metaTypes, attributes)) { // 4.5 嘗試從注解的"value"屬性中獲取bean名稱 Object value = attributes.get("value"); if (value instanceof String) { String strVal = (String)value; // 4.6 檢查獲取到的名稱是否為有效字符串 if (StringUtils.hasLength(strVal)) { // 4.7 如果已經(jīng)存在bean名稱并且與當(dāng)前獲取到的名稱不一致,則拋出異常 if (beanName != null && !strVal.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent component names: '" + beanName + "' versus '" + strVal + "'"); } // 4.8 設(shè)置bean名稱為獲取到的名稱 beanName = strVal; } } } } } // 5. 返回獲取到的bean名稱,如果沒有找到有效名稱,則返回null return beanName; }
拆成代碼塊分析:
protected String buildDefaultBeanName(BeanDefinition definition) { // 1. 從bean定義中獲取bean的類名 String beanClassName = definition.getBeanClassName(); // 2. 確保bean類名已設(shè)置,否則會拋出異常 Assert.state(beanClassName != null, "No bean class name set"); // 3. 使用Spring的ClassUtils獲取類的簡單名稱,即不帶包名的類名 String shortClassName = ClassUtils.getShortName(beanClassName); // 4. 使用Java內(nèi)省工具(Introspector)將類名首字母轉(zhuǎn)換為小寫 // 這就是Spring的默認(rèn)bean命名策略,如果用戶沒有通過@Component等注解顯式指定bean名, // 則會使用該類的非限定類名(即不帶包名的類名),并將首字母轉(zhuǎn)換為小寫作為bean名。 return Introspector.decapitalize(shortClassName); }8.2 生成默認(rèn) bean 名稱的特殊情況 大家肯定知道UserService默認(rèn)bean名稱為userService,但如果類名為MService,bean名稱還是MService,不會首字母小寫。具體原因,我們來分析一下。 我們上面分析buildDefaultBeanName方法生成默認(rèn)bean名稱的時(shí)候,發(fā)現(xiàn)里面有調(diào)用decapitalize方法后再返回,我們來看看decapitalize方法。
提出代碼塊分析一下
/** * 將字符串轉(zhuǎn)換為正常的 Java 變量名規(guī)則的形式。 * 這通常意味著將第一個(gè)字符從大寫轉(zhuǎn)換為小寫, * 但在(不常見的)特殊情況下,當(dāng)有多個(gè)字符并且第一個(gè)和第二個(gè)字符都是大寫時(shí),我們將保持原樣。 * 因此,“FooBah”變?yōu)椤癴ooBah”,“X”變?yōu)椤皒”,但“URL”保持為“URL”。 * 這是 Java 內(nèi)省機(jī)制的一部分,因?yàn)樗婕?Java 對類名和變量名的默認(rèn)命名規(guī)則。 * 根據(jù)這個(gè)規(guī)則,我們可以從類名自動(dòng)生成默認(rèn)的變量名。 * * @param name 要小寫的字符串。 * @return 小寫版本的字符串。 */ public static String decapitalize(String name) { if (name == null || name.length() == 0) { return name; } // 如果字符串的前兩個(gè)字符都是大寫,那么保持原樣 if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) { return name; } char chars[] = name.toCharArray(); // 將第一個(gè)字符轉(zhuǎn)為小寫 chars[0] = Character.toLowerCase(chars[0]); return new String(chars); }根據(jù)Java的命名規(guī)則,類名的首字母應(yīng)該大寫,而變量名的首字母應(yīng)該小寫,它告訴內(nèi)省機(jī)制如何從類名生成默認(rèn)的變量名(或者說bean名)。 這里可以看到,decapitalize方法接收一個(gè)字符串參數(shù),然后將這個(gè)字符串的首字母轉(zhuǎn)為小寫,除非這個(gè)字符串的前兩個(gè)字符都是大寫,這種情況下,字符串保持不變。 所以,在Java內(nèi)省機(jī)制中,如果類名的前兩個(gè)字母都是大寫,那么在進(jìn)行首字母小寫的轉(zhuǎn)換時(shí),會保持原樣不變。也就是說,對于這種情況,bean的名稱和類名是一樣的。 這種設(shè)計(jì)是為了遵守Java中的命名約定,即當(dāng)一個(gè)詞作為類名的開始并且全部大寫時(shí)(如URL,HTTP),應(yīng)保持其全部大寫的格式。
9. Java 的內(nèi)省機(jī)制在生成默認(rèn) bean 名稱中的應(yīng)用
Java內(nèi)省機(jī)制(Introspection)是Java語言對Bean類的一種自我檢查的能力,它屬于Java反射的一個(gè)重要補(bǔ)充。它允許Java程序在運(yùn)行時(shí)獲取Bean類的類型信息以及Bean的屬性和方法的信息。注意:“內(nèi)省” 發(fā)音是"nèi xǐng"。 內(nèi)省機(jī)制的目的在于提供一套統(tǒng)一的API,可以在運(yùn)行時(shí)動(dòng)態(tài)獲取類的各種信息,主要涵蓋以下幾個(gè)方面:
獲取類的類型信息:可以在運(yùn)行時(shí)獲取任意一個(gè)Bean對象所屬的類、接口、父類、修飾符等信息。
屬性信息:可以獲取Bean類的屬性的各種信息,如類型、修飾符等。
獲取方法信息:可以獲取Bean類的方法信息,如返回值類型、參數(shù)類型、修飾符等。
調(diào)用方法:可以在運(yùn)行時(shí)調(diào)用任意一個(gè)Bean對象的方法。
修改屬性值:可以在運(yùn)行時(shí)修改Bean的屬性值。
通過這些反射API,我們可以以一種統(tǒng)一的方式來操作任意一個(gè)對象,無需對對象的具體類進(jìn)行硬編碼。 在命名規(guī)則上,當(dāng)我們獲取一個(gè)Bean的屬性名時(shí),如果相應(yīng)的getter或setter方法的名稱除去"get"/"set"前綴后,剩余部分的第一個(gè)字母是大寫的,那么在轉(zhuǎn)換成屬性名時(shí),會將這個(gè)字母變?yōu)樾?。如果剩余部分的前兩個(gè)字母都是大寫的,屬性名會保持原樣不變,不會將它們轉(zhuǎn)換為小寫。 這個(gè)規(guī)則主要是為了處理一些類名或方法名使用大寫字母縮寫的情況。例如,對于一個(gè)名為 "getURL“的方法,我們會得到”URL“作為屬性名,而不是”uRL"。 雖然在日常開發(fā)中我們可能不會直接頻繁使用到Java的內(nèi)省機(jī)制,但在一些特定的場景和工具中,內(nèi)省機(jī)制卻發(fā)揮著重要作用:
IDE 和調(diào)試工具:這些工具需要利用內(nèi)省機(jī)制來獲取類的信息,如類的層次結(jié)構(gòu)、方法和屬性信息等,以便提供代碼補(bǔ)全、代碼檢查等功能。
測試框架:例如JUnit這樣的測試框架需要通過內(nèi)省機(jī)制來實(shí)例化測試類,獲取測試方法等信息以進(jìn)行測試的運(yùn)行。
依賴注入框架:比如Spring等依賴注入框架需要利用內(nèi)省機(jī)制來掃描類,獲取類中的依賴關(guān)系定義,并自動(dòng)裝配bean。
序列化 / 反序列化:序列化需要獲取對象的類型信息和屬性信息來實(shí)現(xiàn)對象狀態(tài)的持久化;反序列化需要利用類型信息來還原對象。
日志框架:很多日志框架可以通過內(nèi)省機(jī)制自動(dòng)獲取日志方法所在類、方法名等上下文信息。
訪問權(quán)限判斷:一些安全相關(guān)的框架需要通過內(nèi)省判斷一個(gè)成員的訪問權(quán)限是否合法。
面向接口編程:內(nèi)省機(jī)制使得在面向接口編程的時(shí)候可以不需要hardcode接口的實(shí)現(xiàn)類名,而是在運(yùn)行時(shí)定位。
簡言之,內(nèi)省機(jī)制的目的是實(shí)現(xiàn)跨類的動(dòng)態(tài)操作和信息訪問,提高運(yùn)行時(shí)的靈活性。這也使得框架在不知道具體類的情況下,可以進(jìn)行一些有用的操作。
審核編輯:劉清
-
控制器
+關(guān)注
關(guān)注
112文章
16105瀏覽量
177080 -
編譯器
+關(guān)注
關(guān)注
1文章
1617瀏覽量
49016 -
JAVA語言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20062 -
過濾器
+關(guān)注
關(guān)注
1文章
427瀏覽量
19521 -
調(diào)試器
+關(guān)注
關(guān)注
1文章
300瀏覽量
23668
原文標(biāo)題:解鎖Spring組件掃描的新視角
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論