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 "";
}

更多精彩內容歡迎關注我的微信公衆號

在這裏插入圖片描述

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