Spring是如何加载资源的

一直很好奇Spring 是如何读取那么多class文件的。
经过一番探索,不卖关子,结果就在 类ClassPathScanningCandidateComponentProvider之中。

如果同学们没时间细看,我可以直接告诉大家结论:Spring是通过封装Jvm 的 ClassLoader.getResources(String name)来加载资源的(包括ResourceLoader体系)。其实本人见到的很多框架的主要加载资源的手段也是通过ClassLoader.getResources() 来加载资源的。

接下来,我将介绍如何Spring是如何加载资源的。这里需要上一期Spring Environment体系的知识。没看过大家可以关注我的微信号程序袁小黑到理论知识目录查找Spring Environment体系的文章。


先给大家演示一下 类ClassPathScanningCandidateComponentProvider是如何使用的。

我现在有个需求:我想自己读取 com.xiaohei 目录下的 所有BaseEsDao ,BaseBizEsDao 子类的class文件,并且exclude掉BaseBizDao.class, BaseBizEsDao.class自己。并且帮我把每个文件解析成BeanDefinition(BeanDefinition描述bean非常完整,平时我们也可以常常使用。)

针对上面的需求,我只需要参考一下 ClassPathScanningCandidateComponentProvider 的子类 ClassPathBeanDefinitionScanner 即可。

class DaoComponentProvider extends ClassPathScanningCandidateComponentProvider {private final BeanDefinitionRegistry registry;public DaoComponentProvider(Iterable<? extends TypeFilter> includeFilters, BeanDefinitionRegistry registry) {
      super(false); // 是否使用默认的过滤规则,这里写false
      Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
  
      this.registry = registry;//includeFilters 是暴露给用户,让用户告知 provider哪些规则的class需要被加载
      if (includeFilters.iterator().hasNext()) {
         for (TypeFilter filter : includeFilters) {
            addIncludeFilter(filter);
         }
      } else {
         //dao 过滤器,要继承了BaseBizEsDao,或者BaseEsDao的类才行。
         super.addIncludeFilter(new AssignableTypeFilter(BaseBizEsDao.class));
         super.addIncludeFilter(new AssignableTypeFilter(BaseEsDao.class));
         super.addExcludeFilter(new ClassExcludeFilter(BaseEsDao.class,BaseBizEsDao.class));
      }}/**
    * dao 过滤器,要继承了BaseBizEsDao,或者BaseEsDao的类才行。
    */
   @Override
   public void addIncludeFilter(@NonNull TypeFilter includeFilter) {
      super.addIncludeFilter(includeFilter);
   }/**
    * 这个接口是用来查找最后BeanDefinition结果的。
    * 这里可以忽略,只是为了给大家看暴露出来
    */
   @Override
   @NonNull
   public Set<BeanDefinition> findCandidateComponents(@NonNull String basePackage) {
      return super.findCandidateComponents(basePackage);
   }@Nonnull
   @Override
   protected BeanDefinitionRegistry getRegistry() {
      return registry;
   }/**
    * 去掉针对的class,争对某个类的过滤器
    */
   private static class ClassExcludeFilter extends AbstractTypeHierarchyTraversingFilter {
      private final Set<String> classNames = new HashSet<>();ClassExcludeFilter(Object... sources) {
         super(false, false);
         for (Object source : sources) {
            if (source instanceof Class<?>) {
               this.classNames.add(((Class<?>) source).getName());
            }
         }
      }protected boolean matchClassName(@NonNull String className) {
         return this.classNames.contains(className);
      }
   }
}

上面是实现,我们需要扫描下面的代码

public class PersonDao extends BaseBizEsDao<PersonEo> {
}public class OverdueDao extends BaseEsDao {
}

下面带大家看看测试代码:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
private DaoComponentProvider daoComponentProvider = new DaoComponentProvider(Collections.emptySet(), applicationContext);@Test
public void testScanPath() {
    Set<BeanDefinition> candidateComponents = daoComponentProvider.findCandidateComponents("com.yuanxiaohei");
    Assert.assertNotNull(candidateComponents);
    candidateComponents.parallelStream().forEach(c -> {
        Assert.assertNotSame(BaseBizEsDao.class.getName(), c.getBeanClassName());
        Assert.assertNotSame(BaseEsDao.class.getName(), c.getBeanClassName());
    });
    Assert.assertTrue(candidateComponents.parallelStream().anyMatch(c -> Objects.equals(c.getBeanClassName(), PersonDao.class.getName())));
}

上面的结果当然是测试用例通过。

在这里插入图片描述

这表示我们读取到PersonDao ,OverdueDao 两个类并解析成Bean Definition了。看下面的debug框。

在这里插入图片描述

如果大家不想了解原理,就可以到这里为止了,到这里已经可以满足一般的使用了。对源码感兴趣的朋友就可以看后面的文章。

我们顺着org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents 来看看

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
        // 暂时忽略这个代码
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
        // 我们一般的扫描会进入这里
        return scanCandidateComponents(basePackage);
    }
}

查看scanCandidateComponents这段代码(精简过)

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);  // 是这里去这些扫描
        // private ResourcePatternResolver getResourcePatternResolver() {
        //		if (this.resourcePatternResolver == null) {
        //			this.resourcePatternResolver = new PathMatchingResourcePatternResolver();
        //		}
        //		return this.resourcePatternResolver;
        //	}
        for (Resource resource : resources) { //这里处理扫描的结果。
            if (resource.isReadable()) {
                try {
                    MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); // 元数据读取
                    if (isCandidateComponent(metadataReader)) { //看看是否是需要的注解,这里细看会发现它只对@Component进行了处理,为什么呢?卖个关子。
                        ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); //解析成低层次的BeanDefinition
                        sbd.setSource(resource);
                        if (isCandidateComponent(sbd)) { 
                            candidates.add(sbd);
                        }
                    }
                }
                catch (Throwable ex) {
                    throw new BeanDefinitionStoreException(
                        "Failed to read candidate component class: " + resource, ex);
                }
            }
        }
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
}

从上面的代码可以看出 上述是通过 Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); 去加载资源的。getResourcePatternResolver()的实现在上面的注解也写了。

那么默认Spring使用的是 PathMatchingResourcePatternResolver 来读取资源的。

接下来就需要分析 PathMatchingResourcePatternResolver

在这里插入图片描述

从上面我们可以看出 PathMatchingResourcePatternResolver 也是 ResourceLoader 体系的一个。后面我们会介绍ApplicationContext也是一ResourceLoader体系的一部分。ApplicationContext 算是 ResourceLoader的装饰(装饰模式)。

//构造函数

	/**
	 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
	 * <p>ClassLoader access will happen via the thread context class loader.
	 * @see org.springframework.core.io.DefaultResourceLoader
	 */
	public PathMatchingResourcePatternResolver() {
		this.resourceLoader = new DefaultResourceLoader();
	}

	public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
		Assert.notNull(resourceLoader, "ResourceLoader must not be null");
		this.resourceLoader = resourceLoader;
	}

这里调用的是第二个,不过和第一个在这个案例也没什么区别。这样AbstractApplicationContext就能拿到这个资源解析器。

这里开启新的篇章ApplicationContext的第一个重要部分:DefaultResourceLoader

/**
 * Default implementation of the {@link ResourceLoader} interface.
 * Used by {@link ResourceEditor}, and serves as base class for
 * {@link org.springframework.context.support.AbstractApplicationContext}.
 * Can also be used standalone.
 *
 * <p>Will return a {@link UrlResource} if the location value is a URL,
 * and a {@link ClassPathResource} if it is a non-URL path or a
 * "classpath:" pseudo-URL.
 *
 * @author Juergen Hoeller
 * @since 10.03.2004
 * @see FileSystemResourceLoader
 * @see org.springframework.context.support.ClassPathXmlApplicationContext
 */
public class DefaultResourceLoader implements ResourceLoader {
    

根据上面可知,这个DefaultResourceLoader类在PropertyEditor有用到,我们先试试这个类。

看看这个类的源码,没多少就全贴上吧

/**
 * {@link java.beans.PropertyEditor Editor} for {@link Resource}
 * descriptors, to automatically convert {@code String} locations
 * e.g. {@code file:C:/myfile.txt} or {@code classpath:myfile.txt} to
 * {@code Resource} properties instead of using a {@code String} location property.
 *
 * <p>The path may contain {@code ${...}} placeholders, to be
 * resolved as {@link org.springframework.core.env.Environment} properties:
 * e.g. {@code ${user.dir}}. Unresolvable placeholders are ignored by default.
 *
 * <p>Delegates to a {@link ResourceLoader} to do the heavy lifting,
 * by default using a {@link DefaultResourceLoader}.
 *
 * @author Juergen Hoeller
 * @author Dave Syer
 * @author Chris Beams
 * @since 28.12.2003
 * @see Resource
 * @see ResourceLoader
 * @see DefaultResourceLoader
 * @see PropertyResolver#resolvePlaceholders
 */
public class ResourceEditor extends PropertyEditorSupport {

	private final ResourceLoader resourceLoader;

	@Nullable
	private PropertyResolver propertyResolver;

	private final boolean ignoreUnresolvablePlaceholders;


	/**
	 * Create a new instance of the {@link ResourceEditor} class
	 * using a {@link DefaultResourceLoader} and {@link StandardEnvironment}.
	 */
	public ResourceEditor() {
		this(new DefaultResourceLoader(), null);
	}

	/**
	 * Create a new instance of the {@link ResourceEditor} class
	 * using the given {@link ResourceLoader} and {@link PropertyResolver}.
	 * @param resourceLoader the {@code ResourceLoader} to use
	 * @param propertyResolver the {@code PropertyResolver} to use
	 */
	public ResourceEditor(ResourceLoader resourceLoader, @Nullable PropertyResolver propertyResolver) {
		this(resourceLoader, propertyResolver, true);
	}

	/**
	 * Create a new instance of the {@link ResourceEditor} class
	 * using the given {@link ResourceLoader}.
	 * @param resourceLoader the {@code ResourceLoader} to use
	 * @param propertyResolver the {@code PropertyResolver} to use
	 * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders
	 * if no corresponding property could be found in the given {@code propertyResolver}
	 */
	public ResourceEditor(ResourceLoader resourceLoader, @Nullable PropertyResolver propertyResolver,
			boolean ignoreUnresolvablePlaceholders) {

		Assert.notNull(resourceLoader, "ResourceLoader must not be null");
		this.resourceLoader = resourceLoader;
		this.propertyResolver = propertyResolver;
		this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
	}


	@Override
	public void setAsText(String text) {
		if (StringUtils.hasText(text)) {
			String locationToUse = resolvePath(text).trim();
			setValue(this.resourceLoader.getResource(locationToUse));
		}
		else {
			setValue(null);
		}
	}

	/**
	 * Resolve the given path, replacing placeholders with corresponding
	 * property values from the {@code environment} if necessary.
	 * @param path the original file path
	 * @return the resolved file path
	 * @see PropertyResolver#resolvePlaceholders
	 * @see PropertyResolver#resolveRequiredPlaceholders
	 */
	protected String resolvePath(String path) {
		if (this.propertyResolver == null) {
			this.propertyResolver = new StandardEnvironment();
		}
		return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) :
				this.propertyResolver.resolveRequiredPlaceholders(path));
	}


	@Override
	@Nullable
	public String getAsText() {
		Resource value = (Resource) getValue();
		try {
			// Try to determine URL for resource.
			return (value != null ? value.getURL().toExternalForm() : "");
		}
		catch (IOException ex) {
			// Couldn't determine resource URL - return null to indicate
			// that there is no appropriate text representation.
			return null;
		}
	}

}

看上面的描述,只要我们给他一个地址,类似{@code file:C:/myfile.txt} 或者 {@code classpath:myfile.txt}这样格式的代码,它就会自动给我们解析成对应的Resource。使用的ResourceLoader是我们关注的DefaultResourceLoader。

// spring测试的源码
class ResourceEditorTests {

	@Test
	void sunnyDay() {
		PropertyEditor editor = new ResourceEditor();
		editor.setAsText("classpath:org/springframework/core/io/ResourceEditorTests.class");
		Resource resource = (Resource) editor.getValue();
		assertThat(resource).isNotNull();
		assertThat(resource.exists()).isTrue();
	}

跟踪这个代码。进入setAsText方法

@Override
public void setAsText(String text) {
    if (StringUtils.hasText(text)) {  // 判断输入是不是空串
        String locationToUse = resolvePath(text).trim();  // 看下面的调用可知:propertyResolver是StandardEnvironment类型解析。这里只用它解决了占位符的问题
        setValue(this.resourceLoader.getResource(locationToUse));
    }
    else {
        setValue(null);
    }
}


/**
	 * Resolve the given path, replacing placeholders with corresponding
	 * property values from the {@code environment} if necessary.
	 * @param path the original file path
	 * @return the resolved file path
	 * @see PropertyResolver#resolvePlaceholders
	 * @see PropertyResolver#resolveRequiredPlaceholders
	 */
protected String resolvePath(String path) {
    if (this.propertyResolver == null) {
        this.propertyResolver = new StandardEnvironment();
    }
    return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) :
            this.propertyResolver.resolveRequiredPlaceholders(path));
}

//setValue来自父类的java.beans.PropertyEditorSupport
private Object value;
/**
     * Set (or change) the object that is to be edited.
     *
     * @param value The new target object to be edited.  Note that this
     *     object should not be modified by the PropertyEditor, rather
     *     the PropertyEditor should create a new object to hold any
     *     modified value.
     */
public void setValue(Object value) {
    this.value = value;
    firePropertyChange();
}

// 最重点的部分:this.resourceLoader.getResource(locationToUse)
// 这个是调用了org.springframework.core.io.DefaultResourceLoader#getResource 
// DefaultResourceLoader 就是我们最关注的类了。
@Override
public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");

    for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }

    if (location.startsWith("/")) {
        return getResourceByPath(location);
    }
    else if (location.startsWith(CLASSPATH_URL_PREFIX)) { // CLASSPATH_URL_PREFIX = "classpath:";
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    else {
        try {
            // Try to parse the location as a URL...
            URL url = new URL(location);
            return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
        }
        catch (MalformedURLException ex) {
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}

这里可以看出最后只是返回了一个ClassPathResource实现给前端。其实这里使用的是策略模式:

策略:1. 网络策略,2. “/”开头策略,3. “classpath:”开头策略 4. 输入的字符串能指定到对应的文件。所以DefaultResourceLoader 最精华的部分就是getResource了。

看到这里就完了吗?不会!回头再看看这个测试用例:

@Test
void sunnyDay() {
    PropertyEditor editor = new ResourceEditor();
    editor.setAsText("classpath:org/springframework/core/io/ResourceEditorTests.class");
    Resource resource = (Resource) editor.getValue(); //获取value,能根据名字猜到实现,就不说了。
    assertThat(resource).isNotNull(); // 资源是空判断。
    assertThat(resource.exists()).isTrue();  // 直接看到这里。
}

resource.exists() 这个方法的执行如下:

ClassPathResource文件

@Override
public boolean exists() {
    return (resolveURL() != null);
}

/**
	 * Resolves a URL for the underlying class path resource.
	 * @return the resolved URL, or {@code null} if not resolvable
	 */
@Nullable
protected URL resolveURL() {
    if (this.clazz != null) { //这个为null,我们 使用的构造函数没对clazz赋值。
        return this.clazz.getResource(this.path);
    }
    else if (this.classLoader != null) {
        return this.classLoader.getResource(this.path); //最根本的查找资源的方法。
    }
    else {
        return ClassLoader.getSystemResource(this.path);
    }
}

终于找到源头了:

    /**
     * Finds the resource with the given name.  A resource is some data
     * (images, audio, text, etc) that can be accessed by class code in a way
     * that is independent of the location of the code.
     *
     * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
     * identifies the resource.
     *
     * <p> This method will first search the parent class loader for the
     * resource; if the parent is <tt>null</tt> the path of the class loader
     * built-in to the virtual machine is searched.  That failing, this method
     * will invoke {@link #findResource(String)} to find the resource.  </p>
     *
     * @apiNote When overriding this method it is recommended that an
     * implementation ensures that any delegation is consistent with the {@link
     * #getResources(java.lang.String) getResources(String)} method.
     *
     * @param  name
     *         The resource name
     *
     * @return  A <tt>URL</tt> object for reading the resource, or
     *          <tt>null</tt> if the resource could not be found or the invoker
     *          doesn't have adequate  privileges to get the resource.
     *
     * @since  1.1
     */
    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name); // 双亲委托机制模式。
        } else {
            url = getBootstrapResource(name); //到达系统启动类加载器
        }
        if (url == null) {
            url = findResource(name); //系统启动类加载器没有加载到,递归回退到第一次调用然后是扩展类加载器//最后如果都没有加载到,双亲委派加载失败,则加载应用本身自己的加载器。
        }
        return url;
    }

class.getResources 和classLoader.getResources两者使用及区别可以看这里:https://cloud.tencent.com/developer/article/1425180。原理还不大懂。

先记下这个能找资源,其它的后面再查找原理。

回头看PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver是ResourceLoader继承体系的一部分。这部分在上面分析过了。其中最主要的方法如下:

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    //classpath:
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // a class path resource (multiple resources for same name possible)
        //matcher是一个AntPathMatcher对象
        if (getPathMatcher().isPattern(locationPattern
            .substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            return findPathMatchingResources(locationPattern);
        } else {
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern
                .substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    } else {
        // Only look for a pattern after a prefix here
        // (to not get fooled by a pattern symbol in a strange prefix).
        int prefixEnd = locationPattern.indexOf(":") + 1;
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            return findPathMatchingResources(locationPattern);
        }
        else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

// 比如locationPattern=classpath*:org/springframework/context/annotation6/**/*.class
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    //rootDirPath=classpath*:org/springframework/context/annotation6/
    String rootDirPath = determineRootDir(locationPattern);
    //subPattern = **/*.class
    String subPattern = locationPattern.substring(rootDirPath.length());
    Resource[] rootDirResources = getResources(rootDirPath);  // 这里调用自身,然后通过classpath*:org/springframework/context/annotation6/ 回去找这个目录下所有的文件夹资源。通过findAllClassPathResources方法。
    Set<Resource> result = new LinkedHashSet<>(16);
    for (Resource rootDirResource : rootDirResources) {
        rootDirResource = resolveRootDirResource(rootDirResource); //
        URL rootDirUrl = rootDirResource.getURL();
        if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
            URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
            if (resolvedUrl != null) {
                rootDirUrl = resolvedUrl;
            }
            rootDirResource = new UrlResource(rootDirUrl);
        }
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }
        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        }
        else {
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); // 一般情况的寻找文件夹下所有文件会进入这里,这里面的逻辑很深,就不举行扩张了。
        }
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
    }
    // 这里的返回值可以给大家看看,如下:
    return result.toArray(new Resource[0]);
}


protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    return result.toArray(new Resource[0]);
}

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet<>(16);
    ClassLoader cl = getClassLoader();
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        result.add(convertClassLoaderURL(url));
    }
    if ("".equals(path)) {
        // The above result is likely to be incomplete, i.e. only containing file system references.
        // We need to have pointers to each of the jar files on the classpath as well...
        addAllClassLoaderJarRoots(cl, result);
    }
    return result;
}

findPathMatchingResources的返回值:

在这里插入图片描述

从上面可以看出其实最主要的就是通过ClassLoader.getResources找资源。

isPattern:

@Override
public boolean isPattern(String path) {
    return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
}
class PathMatchingResourcePatternResolverTests {

	private static final String[] CLASSES_IN_CORE_IO_SUPPORT =
			new String[] {"EncodedResource.class", "LocalizedResourceHelper.class",
					"PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class",
					"PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class",
					"ResourcePatternResolver.class", "ResourcePatternUtils.class"};

	private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT =
			new String[] {"PathMatchingResourcePatternResolverTests.class"};

	private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATIONS =
			new String[] {"NonNull.class", "NonNullApi.class", "Nullable.class"};

	private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();


	@Test
	void invalidPrefixWithPatternElementInIt() throws IOException {
		assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() ->
				resolver.getResources("xx**:**/*.xy"));
	}

	@Test
	void singleResourceOnFileSystem() throws IOException {
		Resource[] resources =
				resolver.getResources("org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.class");
		assertThat(resources.length).isEqualTo(1);
		assertProtocolAndFilenames(resources, "file", "PathMatchingResourcePatternResolverTests.class");
	}

	@Test
	void singleResourceInJar() throws IOException {
		Resource[] resources = resolver.getResources("org/reactivestreams/Publisher.class");
		assertThat(resources.length).isEqualTo(1);
		assertProtocolAndFilenames(resources, "jar", "Publisher.class");
	}

	@Disabled
	@Test
	void classpathStarWithPatternOnFileSystem() throws IOException {
		Resource[] resources = resolver.getResources("classpath*:org/springframework/core/io/sup*/*.class");
		// Have to exclude Clover-generated class files here,
		// as we might be running as part of a Clover test run.
		List<Resource> noCloverResources = new ArrayList<>();
		for (Resource resource : resources) {
			if (!resource.getFilename().contains("$__CLOVER_")) {
				noCloverResources.add(resource);
			}
		}
		resources = noCloverResources.toArray(new Resource[0]);
		assertProtocolAndFilenames(resources, "file",
				StringUtils.concatenateStringArrays(CLASSES_IN_CORE_IO_SUPPORT, TEST_CLASSES_IN_CORE_IO_SUPPORT));
	}

	@Test
	void getResourcesOnFileSystemContainingHashtagsInTheirFileNames() throws IOException {
		Resource[] resources = resolver.getResources("classpath*:org/springframework/core/io/**/resource#test*.txt");
		assertThat(resources).extracting(Resource::getFile).extracting(File::getName)
			.containsExactlyInAnyOrder("resource#test1.txt", "resource#test2.txt");
	}

	@Test
	void classpathWithPatternInJar() throws IOException {
		Resource[] resources = resolver.getResources("classpath:reactor/util/annotation/*.class");
		assertProtocolAndFilenames(resources, "jar", CLASSES_IN_REACTOR_UTIL_ANNOTATIONS);
	}

	@Test
	void classpathStarWithPatternInJar() throws IOException {
		Resource[] resources = resolver.getResources("classpath*:reactor/util/annotation/*.class");
		assertProtocolAndFilenames(resources, "jar", CLASSES_IN_REACTOR_UTIL_ANNOTATIONS);
	}

	@Test
	void rootPatternRetrievalInJarFiles() throws IOException {
		Resource[] resources = resolver.getResources("classpath*:*.dtd");
		boolean found = false;
		for (Resource resource : resources) {
			if (resource.getFilename().equals("aspectj_1_5_0.dtd")) {
				found = true;
				break;
			}
		}
		assertThat(found).as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar").isTrue();
	}


	private void assertProtocolAndFilenames(Resource[] resources, String protocol, String... filenames)
			throws IOException {

		// Uncomment the following if you encounter problems with matching against the file system
		// It shows file locations.
//		String[] actualNames = new String[resources.length];
//		for (int i = 0; i < resources.length; i++) {
//			actualNames[i] = resources[i].getFilename();
//		}
//		List sortedActualNames = new LinkedList(Arrays.asList(actualNames));
//		List expectedNames = new LinkedList(Arrays.asList(fileNames));
//		Collections.sort(sortedActualNames);
//		Collections.sort(expectedNames);
//
//		System.out.println("-----------");
//		System.out.println("Expected: " + StringUtils.collectionToCommaDelimitedString(expectedNames));
//		System.out.println("Actual: " + StringUtils.collectionToCommaDelimitedString(sortedActualNames));
//		for (int i = 0; i < resources.length; i++) {
//			System.out.println(resources[i]);
//		}

		assertThat(resources.length).as("Correct number of files found").isEqualTo(filenames.length);
		for (Resource resource : resources) {
			String actualProtocol = resource.getURL().getProtocol();
			assertThat(actualProtocol).isEqualTo(protocol);
			assertFilenameIn(resource, filenames);
		}
	}

	private void assertFilenameIn(Resource resource, String... filenames) {
		String filename = resource.getFilename();
		assertThat(Arrays.stream(filenames).anyMatch(filename::endsWith)).as(resource + " does not have a filename that matches any of the specified names").isTrue();
	}

}

isCandidateComponent 细看会发现它只对@Component 进行了通过,为什么呢?

因为@Controller, @Repository, @Service都是加上了@Component,算是 @Component 的派生注解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

更多精彩内容欢迎关注我的微信公众号

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值