【課程免費分享】6-Spring中Bean掃描實戰

6、Spring中Bean掃描實戰

當需要掃描bean可以使用@ComponentScan(basePackages="")對指定包下添加的Spring支持的註解的類。SpringBoot是默認會掃描@SpringBootApplication註解所在包和所有子包的類。這樣使用的話對於單純的業務邏輯實現是沒有問題的,但是如果想要把共通實現抽取出來作爲公共項目,或者自定義拓展自己的註解,這時該如何掃描bean呢?

假設你需要使用自定義註解的方式實現某一個功能,或者想用接口的方式實現某一個功能,那麼這時候@ComponentScan就起不了作用了,默認情況包的掃描是不會掃描接口類的,而且自定義的註解也不會被掃描進去。這時候就需要自己對包進行掃描了,Spring提供了幾個非常好的類掃描功能,通過這幾個類可以非常簡單高效的完成我們需要的功能。

PathMatchingResourcePatternResolver

在Spring中,類文件也是一種資源文件,那麼就可以通過PathMatchingResourcePatternResolver進行掃描,
當前工程的目錄結構如下(根據名字可以分辨出類的類型):
enter image description here
入口類爲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還是使用最多的,畢竟這個類封裝了一層,提供更方便的方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章