6、Spring中Bean掃描實戰
當需要掃描bean可以使用@ComponentScan(basePackages="")對指定包下添加的Spring支持的註解的類。SpringBoot是默認會掃描@SpringBootApplication註解所在包和所有子包的類。這樣使用的話對於單純的業務邏輯實現是沒有問題的,但是如果想要把共通實現抽取出來作爲公共項目,或者自定義拓展自己的註解,這時該如何掃描bean呢?
假設你需要使用自定義註解的方式實現某一個功能,或者想用接口的方式實現某一個功能,那麼這時候@ComponentScan就起不了作用了,默認情況包的掃描是不會掃描接口類的,而且自定義的註解也不會被掃描進去。這時候就需要自己對包進行掃描了,Spring提供了幾個非常好的類掃描功能,通過這幾個類可以非常簡單高效的完成我們需要的功能。
PathMatchingResourcePatternResolver
在Spring中,類文件也是一種資源文件,那麼就可以通過PathMatchingResourcePatternResolver進行掃描,
當前工程的目錄結構如下(根據名字可以分辨出類的類型):
入口類爲Application,在Application的main方法中掃描extra這個包下的文件,代碼如下:
public static void main(String[] args) throws Exception {
SpringApplication springApplication = new SpringApplication(Application.class);
ConfigurableApplicationContext application = springApplication.run(args);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:/com/cml/chat/lesson/extra/**.class");
log.info("findResouceSize==>" + Arrays.asList(resources).toString());
}
使用PathMatchingResourcePatternResolver對extra包下所有的資源文件進行掃描,篩選出class文件。這樣包下所有的class都被掃描進來了,但是我們這裏只是獲取到了class文件而已,還得要根據掃描出的結果,對掃描進來的class文件進行校驗,判斷對應的class是否是我們需要的對象,這時候還得class加載進來,這樣的繁瑣操作顯然不適合實際開發需求。正好Spring提供了更好的操作方式ClassPathScanningCandidateComponentProvider,通過這個類可以掃描出需要的類文件,並且可以對掃描出的對象通過字節碼的方式獲取到類的類型。
ClassPathScanningCandidateComponentProvider
ClassPathScanningCandidateComponentProvider也是通過PathMatchingResourcePatternResolver進行文件掃描,但是對其封裝了一層,將掃描到的類通過讀取字節碼的方式獲取到類信息。其核心實現在方法findCandidateComponents中,通過此方法可以掃描出所有需要的對象,並且可以通過isCandidateComponent方法對掃描到的對象進行篩選。核心實現如下:
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
try {
//配置掃描規則,最後生成的是:classpath*:basePackage/**/*.class
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
//resourcePatternResolver爲PathMatchingResourcePatternResolver
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
//打印log
if (resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
//打印log
candidates.add(sbd);
}
else {
//打印log
}
}
else {
//打印log
}
}
catch (Throwable ex) {
//拋出異常
}
}
else {
//打印log
}
}
}
catch (IOException ex) {
//拋出異常...
}
return candidates;
}
主要流程可以歸結如下:
- 通過resourcePatternResolver將需要的class掃描出來
resourcePatternResolver爲PathMatchingResourcePatternResolver實例
- 將掃描出的類信息封裝爲MetadataReader對象
使用ASM框架讀取字節碼獲取class對象信息,將class信息封裝爲AnnotationMetadata對象。ASM信息可以參考:https://www.cnblogs.com/onlysun/p/4533798.html
- isCandidateComponent方法中,對掃描結果進行過濾
回調通過addExcludeFilter,addIncludeFilter方法添加的對象過濾器,進行規則匹配,只有滿足過濾條件的數據纔會進入候選類,進入下一輪篩選。
- 過濾成功後使用isCandidateComponent方法校驗對象是否是我們需要的類
默認是隻掃描普通類和加了@Lookup註解的抽象類的,如果需要掃描接口和抽象類,就需要重寫這個方法,將接口和抽象類添加到候選列表中。代碼如下:
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isAbstract() || metadata.isInterface()));
}
通過以上的步驟最終篩選出的類就是我們需要的對象了。那麼如何使用ClassPathScanningCandidateComponentProvider呢?這裏舉個例子:SimpleBeanScanner在bean初始化完成之後就使用MyClassScanner對資源文件進行掃描,將掃描出的類打印出來。
@Component
public class SimpleBeanScanner implements EnvironmentAware, ResourceLoaderAware {
private static Logger log = LoggerFactory.getLogger(SimpleBeanScanner.class);
private ResourceLoader resourceLoader;
private Environment environment;
@PostConstruct
public void scan() {
log.info("==========================start scan==============================");
MyClassScanner scanner = new MyClassScanner();
scanner.setEnvironment(environment);
scanner.setResourceLoader(resourceLoader);
scanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
log.info(scanner.findCandidateComponents("com.cml.chat.lesson.lesson6").toString());
log.info("==========================end scan==============================");
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
public class MyClassScanner extends ClassPathScanningCandidateComponentProvider {
public MyClassScanner() {
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isAbstract() || metadata.isInterface()));
}
}
通過調用ClassPathScanningCandidateComponentProvider.findCandidateComponents就可以對指定包下的類進行篩選,並且將掃描到的類轉換成Spring中BeanDefinition集合返回。這樣我們就可以非常方便的使用它進行bean掃描,並將掃描到的類通過工廠bean的方式添加到Spring上下文中了。但是添加到Spring上下文中還需要我們手動添加,有沒有更簡便的方式呢?
ClassPathBeanDefinitionScanner
ClassPathBeanDefinitionScanner繼承自ClassPathScanningCandidateComponentProvider,對ClassPathScanningCandidateComponentProvider提供了更高一層的封裝,對外開放scan方法,通過BeanDefinitionRegistry 直接將掃描到的bean對象添加到Spring上下文中。這樣的實現方式對於普通類來說是非常實用的。
如果掃描到了接口對象,這時添加到Spring上下文中就會報錯了。
因爲會自動將bean註冊到Spring上下文中,接口是無法實例化的,所以添加接口時需要使用工廠bean的方式
如果需要自定義掃描的話只需要繼承ClassPathBeanDefinitionScanner就可以了,代碼如下:
public class MyClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
public MyClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isConcrete() || metadata.isAbstract() || metadata.isInterface()));
}
}
如果使用@Import導入的方式,只需要實現ImportBeanDefinitionRegistrar接口即可。代碼如下:
public class MyClassPathBeanDefinitionScannerEntrance2 implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MyClassPathBeanDefinitionScanner scanner = new MyClassPathBeanDefinitionScanner(registry);
scanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
System.out.println("========MyClassPathBeanDefinitionScannerEntrance2==========>" + scanner.scan("com.cml.chat.lesson.extra"));
}
}
SpringBoot使用的bean工廠爲DefaultListableBeanFactory,因爲DefaultListableBeanFactory實現了BeanDefinitionRegistry接口,所以可以先獲取到bean工廠再進行掃描。當然也可以從BeanDefinitionRegistry註釋中得知。
Spring’s bean definition readers expect to work on an implementation of this interface. Known implementors within the Spring core are DefaultListableBeanFactory and GenericApplicationContext.
如果使用@Component註解的方式,則需要獲取到Bean工廠,只需要實現BeanFactoryAware接口即可,至於BeanFactoryAware的原理前面的文章《Spring各種Aware注入的原理與實戰》已經詳細說明了。
@Component
public class MyClassPathBeanDefinitionScannerEntrance implements BeanFactoryAware {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
MyClassPathBeanDefinitionScanner scanner = new MyClassPathBeanDefinitionScanner((BeanDefinitionRegistry) beanFactory);
//所有的類篩選進來
scanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
System.out.println("========MyClassPathBeanDefinitionScannerEntrance==========>" + scanner.scan("com.cml.chat.lesson.extra"));
}
}
總結
這裏對上文中提供掃描類功能的幾個類進行總結:
- @ComponentScan
具有一定的侷限性,只能識別Spring中內置的一些註解類,適合項目業務邏輯開發,不適合架構類的項目使用。
- PathMatchingResourcePatternResolver
基於Spring的強大的資源掃描器,可以對工程中的任意類型文件數據進行掃描,但是隻是掃描到了對應的資源文件,並不能提供對資源文件的類型的校驗。這個適合用戶資源文件的掃描,比如Properties文件等
- ClassPathScanningCandidateComponentProvider
通過封裝PathMatchingResourcePatternResolver,篩選出PathMatchingResourcePatternResolver掃描出的類文件,並通過字節碼的方式判斷對應類的類型。將掃描到的類對象轉換成BeanDefinition,方便導入到上下文中。
- ClassPathBeanDefinitionScanner
通過封裝ClassPathScanningCandidateComponentProvider,將掃描出的BeanDefinition集合添加到Spring上下文中。對於框架類掃描功能,這個類還是非常實用的。
以上幾個類都能實現資源文件的掃描功能,但是各有各的實用場景,通常來說對於類的掃描ClassPathBeanDefinitionScanner還是使用最多的,畢竟這個類封裝了一層,提供更方便的方式。