一直很好奇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 "";
}