Spring扫描注解的功能
我们知道在Spring中可以使用注解声明Bean,可以让我们不用再去配置繁琐的xml文件,确实让我们的开发简便不少。只要在Spring xml里配置如下,就开启了这项功能。
<context:component-scan base-package="com.zhaoyanblog" />
Spring就会自动扫描该包下面所有打了Spring注解的类,帮你初始化病注册到Spring容器中。
public class UserController {
private UserDao userDao;
//TODO
}
上述行为,就和在xml里进行下面的配置是等价的。
<bean class="com.zhaoyanblog.UserController">
<property name="userDao" ref="userDao" />
</bean>
那么问题来了,Spring是怎么扫描到com.zhaoyanblog包下面的所有带注解的类的呢?
context:component-scan标签的处理者
要弄清楚为什么在xml里配置了context:component-scan就可以实现这样的功能,就要现找到这个标签的处理者。我们很容易联想到Spring解析xml的基本原理,就是遇到这个标签以后,交给一个类处理,这个类扫描包下带注解的类,初始化成对象。我们就是要找到这个关键的类。
Spring对Xml的解析功能后面阅读,这里先简要描述。Spring的jar包里有两个重要的配置文件:spring.schemas和spring.handlers,在META-INF目录下。
spring.schemas记录了xml的每个命名空间,对应的Schema校验XSD文件在哪个目录。
http\://www.springframework.org/schema/context/spring-context-4.2.xsd=org/springframework/context/config/spring-context.xsd
http\://www.springframework.org/schema/context/spring-context-4.3.xsd=org/springframework/context/config/spring-context.xsd
http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd
spring.handlers里配置了每个命名空间的标签都由哪个类处理
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
我们可以看到context这个命名空间由org.springframework.context.config.ContextNamespaceHandler这个类处理。打开这个类
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}
这个类为每个标签都给出了一个处理类,component-scan的处理类是ComponentScanBeanDefinitionParser
继续打开ComponentScanBeanDefinitionParser
private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
public BeanDefinition parse(Element element, ParserContext parserContext) {
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
看到这里整明白了,扫描某个包下面的所有类的工作,就是ClassPathBeanDefinitionScanner干的。入口是doScan方法。
查找所有的包路径
ClassPathBeanDefinitionScanner有很多细节,比如可以设置class的filter, 设置classloader等等,我们先关注最主要的功能,就是怎么找到一个包下面所有的类的。
通过调用关系,一路找下去
doScan->findCandidateComponents(super)->scanCandidateComponents(super)
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);
我们发现,原来Spring是在classpath下面,通过查找所有classpath*:com/zhaoyanblog/**/*.class的文件来实现的啊。
那这个问题就变了,Spring是如何在Classpath下面查找满足classpath*:com/zhaoyanblog/**/*.class 匹配条件的文件的呢?
这个就需要看类PathMatchingResourcePatternResolver的源码了。入口getResources方法。
这里需要讲个特别知识点:classpath*: 前缀表达式,用于描述类路径匹配。*表示所有classpath,不带*表示查到的第一个类路径。后面的匹配表达式有个学名: “Ant风格路径表达式”(Ant-style patterns)。这个表达式很简单,可以自行google一下。 这里我们这里只分析下
Spring是如何查找 classpath*:com/zhaoyanblog/**/*.class 这个表达式的,其它以此类推。
PathMatchingResourcePatternResolver代码一路读下去
getResources->findPathMatchingResources
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
看到Spring会把路径分成两部分,一部分是rootDir=classpath*:com/zhaoyanblog/,一部分是subPatter=**/*.class
查找rootDir,又递归getResources,你会发现这次走的是下述分支
getResources->findAllClassPathResources->doFindAllClassPathResources
过程代码很简单,通过classloader.getResources(“com/zhaoyanblog/”) 找到了所以的com.zhaoyanblog包路径。
查找包路径下的类
既然找到classpath的所有com/zhaoyanblog/路径,怎么列举下面的class文件呢?
一个classpath路径,可能是jar包,也可能是个磁盘目录,还有可能是其它形式。
没错,Spring就是这样勤劳的递归遍历每种类型的路径,直到找遍所有满足**/*.class条件的文件。
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));
}
包括osgi的boundle文件路径、jar包文件路径、vfs虚拟文件路径以及最后是普通文件路径。
读取类文件
找到类文件 假设是你,你会怎么办?
识别文件名,根据文件路径,得到类的完整名称:com.zhaoyanblog.UserController
然后Class.forName(“com.zhaoyanblog.UserController”) ,得到class对象是吗?
显然这样做是不对的,也是不道德的。你怎么敢确认这个类就是有Spring注解的类呢?假设这个类没有Spring注解,那么当你执行了Class.forName, 这个类就会被classloader加载,它的静态属性,静态代码段就会被执行。这个后果很有可能不是Spring使用者想看到的。
那怎么办呢?
我们回到ClassPathBeanDefinitionScanner类的最开始的地方
doScan->findCandidateComponents(super)->scanCandidateComponents(super)
private Set<BeanDefinition> scanCandidateComponents(String basePackage) { ... for (Resource resource : resources) { ... if (resource.isReadable()) { try { MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource);
在得到每一个类的resource文件路径以后,会使用一个MetadataReader读取它的元数据信息。
没错,Spring就是自己解析了class文件,主要读取了它的类名,注解信息。
阅读代码路径
CachingMetadataReaderFactory(SimpleMetadataReaderFactory)->SimpleMetadataReader->ClassReader
最终就是一个字节一个字节的解析class文件。至此“Spring扫描注解”的功能代码阅读完毕。
题外小知识点
-
PathMatchingResourcePatternResolver查找classpath*.*xml 和查找classpath*.spring/*xml 的过程是不同的。 在Spring 官方文档里,你会看到这样一句这样的警告:
Note that `classpath*:`, when combined with Ant-style patterns, only works reliably with at least one root directory before the pattern starts, unless the actual target files reside in the file system. This means that a pattern such as `classpath*:*.xml` might not retrieve files from the root of jar files but rather only from the root of expanded directories. Spring’s ability to retrieve classpath entries originates from the JDK’s `ClassLoader.getResources()` method, which only returns file system locations for an empty string (indicating potential roots to search). Spring evaluates `URLClassLoader` runtime configuration and the `java.class.path` manifest in jar files as well, but this is not guaranteed to lead to portable behavior.
具体是说Spring查找classpath下的文件,依赖于JDK的ClassLoader.getResources,但是如果你写成类似classpath*:*.xml
的路径,那么Spring就会先去查询ClassLoader.getResources(“”). 但是JDK的该方法对于“”路径,只会返回一个(你可以试一下哟~)。所以你查找跟路径的文件,有可能查不全。当然Spring也做了一点努力,就是把classpath下面的所以jar包都进行了搜索,但是还是保不齐能搜全。所以你想通过扫描类路径加载文件,最好不要放在跟路径下面。
具体你可以看下面类和方法对“”的处理
org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindAllClassPathResources
-
@Repository @Service @Component 等注解都可以注册一个Bean,有啥区别呢?
看下这些注解的java doc, 比如@Service
* Indicates that an annotated class is a "Service", originally defined by Domain-Driven
* Design (Evans, 2003) as "an operation offered as an interface that stands alone in the
* model, with no encapsulated state."
我靠,这个概念来自2003年Evans的大作《领域驱动设计(DDD)》, 又是一个新的知识领域。后面需要学习。
也就是说这些注解本质上基本没有区别,仅是用于标识该类在DDD模型中的位置。
Spring官方的解释是,准确的使用这些注解,可以让一些开发工具更好的分析你的代码,比如我们最好的开发工具IDEA等。Spring未来很有可能对不同的注解带来不同的附加含义。
比如@Repository,就已经被特殊处理了,Spring会认为@Repository属于持久层的类,针对它的方法抛出的异常,会统一转换成Spring 的数据库异常类DataAccessException
除非注明,赵岩的博客文章均为原创,转载请以链接形式标明本文地址
本文地址:https://zhaoyanblog.com/archives/1080.html