在Spring中扫描package是一个常见的操作,比如扫描package为特定的class创建实例。围绕Spring建立的第三方工具通常也需要扫描package然后向Spring注册特定的实例。比如Mybatis,在启动时会扫描mapper所在的package,为底下定义的mapper生成实例(详见MyBatis探究(三)——MyBatis与Spring的结合及代码探究)。
之前在看Spring代码时,对于package扫描的过程基本就忽视了。最近在写代码时需要这样扫描package的功能,乘着这个机会复习一下Spring中扫描package的功能。
使用Spring来扫描某个指定的package是非常便捷的,示例如下:
1 | ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); |
ClassPathScanningCandidateComponentProvider是Spring中用于扫描package的类,Spring中的扫描任务都是使用这个类或者继承这个类来完成的。
新建ClassPathScanningCandidateComponentProvider时需要传入一个useDefaultFilters参数,该参数表示是否注册默认的过滤器(默认包含几个过滤注解的过滤器)。
调用findCandidateComponents查找指定package下符合要求的类,该方法调用scanCandidateComponents扫描package,步骤如下:
首先根据指定的package生成一个查找路径(packageSearchPath),比如根据io.github.wangqifox生成的查找路径为classpath*:io/github/wangqifox/**/*.class。
然后调用PathMatchingResourcePatternResolver.getResources方法从该查找路径中扫描类:
1 | public Resource[] getResources(String locationPattern) throws IOException { |
首先调用AntPathMatcher.isPattern方法判断路径中是否包含*、?这样的通配符:
1 | public boolean isPattern(String path) { |
我们的查找路径io/github/wangqifox/**/*.class是包含通配符的,因此调用findPathMatchingResources方法扫描路径匹配的类。
findPathMatchingResources首先将查找路径拆成两部分:rootDirPath(根路径,比如classpath*:io/github/wangqifox/)和subPattern(匹配路径,比如**/*.class)。
接着调用getResources(rootDirPath)扫描rootDirPath底下的所有类。getResources方法前面出现过,这次因为传入的路径是根路径不包含通配符,所以调用findAllClassPathResources扫描路径底下所有的类,它委托给doFindAllClassPathResources方法去实现。
doFindAllClassPathResources方法通过类加载器(ClassLoader)去获取指定路径下的所有资源,以URL的形式表示。调用convertClassLoaderURL方法将URL转换成UrlResource的形式返回。
回到findPathMatchingResources方法,现在已经得到根路径下所有的资源了。接下来对根目录下的资源进行筛选。资源一共有4中类型:bundle类型、vfs类型、jar类型、普通文件类型。重点关注jar类型和本地文件类型。
方法ResourceUtils.isJarURL用于判断资源是否是jar类型,jar类型包括jar、war、zip、vfszip、wsjar。如果是jar类型则调用doFindPathMatchingJarResources方法,传入subPattern(匹配路径)参数筛选根路径下的资源。
如果是普通文件则调用doFindPathMatchingFileResources方法,传入subPattern(匹配路径)参数筛选根路径下的资源。
回到scanCandidateComponents方法,经过getResources的调用,获得了指定路径下所有的类资源。接下来要做的就是对所有的资源做进一步的筛选:
- 创建资源元数据的reader:
MetadataReader - 调用
boolean isCandidateComponent(MetadataReader metadataReader)方法判断该资源是否符合候选资源的要求 - 如果资源通过了筛选,再调用
boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition)方法进一步筛选
boolean isCandidateComponent(MetadataReader metadataReader)方法默认遍历excludeFilters和includeFilters过滤器来检查资源是否匹配要求。如果不注册过滤器,返回false,即package下所有的类都不符合要求:
1 | protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { |
boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition)方法默认会筛选掉接口以及内部类:
1 | protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { |
经过上面一系列的步骤,最终返回符合我们需求的类定义(BeanDefinition)。