【课程免费分享】6-Spring中Bean扫描实战

6、Spring中Bean扫描实战

当需要扫描bean可以使用@ComponentScan(basePackages="")对指定包下添加的Spring支持的注解的类。SpringBoot是默认会扫描@SpringBootApplication注解所在包和所有子包的类。这样使用的话对于单纯的业务逻辑实现是没有问题的,但是如果想要把共通实现抽取出来作为公共项目,或者自定义拓展自己的注解,这时该如何扫描bean呢?

假设你需要使用自定义注解的方式实现某一个功能,或者想用接口的方式实现某一个功能,那么这时候@ComponentScan就起不了作用了,默认情况包的扫描是不会扫描接口类的,而且自定义的注解也不会被扫描进去。这时候就需要自己对包进行扫描了,Spring提供了几个非常好的类扫描功能,通过这几个类可以非常简单高效的完成我们需要的功能。

PathMatchingResourcePatternResolver

在Spring中,类文件也是一种资源文件,那么就可以通过PathMatchingResourcePatternResolver进行扫描,
当前工程的目录结构如下(根据名字可以分辨出类的类型):
【课程免费分享】6-Spring中Bean扫描实战_第1张图片
入口类为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 findCandidateComponents(String basePackage) {
	Set candidates = new LinkedHashSet();
	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还是使用最多的,毕竟这个类封装了一层,提供更方便的方式。

你可能感兴趣的:(java)