一直很好奇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 "";
}
更多精彩內容歡迎關注我的微信公衆號