包扫描的过程
测试代码:
// 扫描指定包下的所有类
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
// 扫描指定包下的所有类
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
scanner.scan("com.test.entity");
for (String beanDefinitionName : registry.getBeanDefinitionNames()) {System.err.println(beanDefinitionName);
}
注意:下述源码在阅读时应尽量避免死磕,先梳理整体流程,理解其动作的含义,具体细节可以在看完整体之后再细致打磨。如果整个流程因为一些细节而卡住,那就丧失了看源码的意义,我们需要的是掌握流程和优秀的设计理念,而不是一字一句的抄下来。
scan方法详解
源码如下:翻译通过CodeGeeX进行自动生成并自己进行修改。
public int scan(String... basePackages) {// 获取扫描开始时的BeanDefinition数量int beanCountAtScanStart = this.registry.getBeanDefinitionCount();// 执行扫描doScan(basePackages);// 如果需要,注册注解配置处理器if (this.includeAnnotationConfig) {AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}// 返回扫描结束时的BeanDefinition数量与扫描开始时的数量之差return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
通过上面的代码,我们可以看到核心的方法为doScan,具体源码如下:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {// 确保至少传入一个基础包Assert.notEmpty(basePackages, "At least one base package must be specified");// 创建一个存放BeanDefinitionHolder的集合Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();for (String basePackage : basePackages) {// 1找出候选的组件Set<BeanDefinition> candidates = findCandidateComponents(basePackage);// 遍历候选的组件for (BeanDefinition candidate : candidates) {// 解析组件的作用域元数据ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);// 设置组件的作用域candidate.setScope(scopeMetadata.getScopeName());// 2生成组件的名称String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);// 如果候选的组件是抽象BeanDefinition的实例if (candidate instanceof AbstractBeanDefinition) {// 执行后处理器,对候选的组件进行处理postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);}// 如果候选的组件是带有注解的BeanDefinition的实例if (candidate instanceof AnnotatedBeanDefinition) {// 3处理带有注解的BeanDefinition的公共定义AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);}// 如果检查候选的组件if (checkCandidate(beanName, candidate)) {// 创建一个BeanDefinitionHolder,并将其添加到集合中BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);// 应用代理模式definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);// 将BeanDefinition注册到容器中registerBeanDefinition(definitionHolder, this.registry);}}}// 返回存放BeanDefinitionHolder的集合return beanDefinitions;
}
现在我们一步步的使用debug查看整个doScan的大体过程
1. 通过传入的路径扫描并得到对应的BeanDefinition
截取的部分代码片段如下:
for (String basePackage : basePackages) {// 找出候选的组件Set<BeanDefinition> candidates = findCandidateComponents(basePackage);// other
}
findCandidateComponents方法的源码:
从 Spring 5.0 开始新增了一个 @Indexed 注解(新特性,@Component 注解上面就添加了 @Indexed 注解), 这里不会去扫描指定路径下的 .class 文件,而是读取所有 META-INF/spring.components
文件中符合条件的类名,直接添加 .class 后缀就是编译文件,而不要去扫描。
public Set<BeanDefinition> findCandidateComponents(String basePackage) {// 如果componentsIndex不为空,并且indexSupportsIncludeFilters()为trueif (this.componentsIndex != null && indexSupportsIncludeFilters()) {// 从componentsIndex中添加候选组件return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);}// 否则else {// 扫描候选组件return scanCandidateComponents(basePackage);}
}
我们着重看scanCandidateComponents方法,源码如下:
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {// 创建一个LinkedHashSet,用于存储扫描到的候选组件Set<BeanDefinition> candidates = new LinkedHashSet<>();try {// 1.1拼接包搜索路径String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +resolveBasePackage(basePackage) + '/' + this.resourcePattern;// 1.2获取资源Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);// 判断是否开启日志记录boolean traceEnabled = logger.isTraceEnabled();boolean debugEnabled = logger.isDebugEnabled();for (Resource resource : resources) {if (traceEnabled) {logger.trace("Scanning " + resource);}try {// 1.3获取元数据读取器MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);// 判断是否是候选组件if (isCandidateComponent(metadataReader)) {// 1.4创建一个ScannedGenericBeanDefinition,用于存储元数据读取器ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);sbd.setSource(resource);// 判断是否是候选组件if (isCandidateComponent(sbd)) {if (debugEnabled) {logger.debug("Identified candidate component class: " + resource);}// 将候选组件添加到candidates中candidates.add(sbd);} else {if (debugEnabled) {logger.debug("Ignored because not a concrete top-level class: " + resource);}}} else {if (traceEnabled) {logger.trace("Ignored because not matching any filter: " + resource);}}} catch (FileNotFoundException ex) {if (traceEnabled) {logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage());}} 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;
}
1.1 拼接搜索路径
源码如下:
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +resolveBasePackage(basePackage) + '/' + this.resourcePattern;
简单的字符串拼接,其代码本身没有任何技术含量,但debug的时候,可以通过其显示的值来帮助我们更好的理解项目。
debug的结果为:classpath*:com/test/entity/**/*.class
可以看到,是从根路径的com.test.enetity下开始寻找所有的字节码文件。
1.2 根据包扫描路径获取资源
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
具体方法:
public Resource[] getResources(String locationPattern) throws IOException {// 断言locationPattern不能为空Assert.notNull(locationPattern, "Location pattern must not be null");// 如果locationPattern以CLASSPATH_ALL_URL_PREFIX开头if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {// 是一个类路径资源(同一个名称可能存在多个资源)if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {// 是一个类路径资源模式return findPathMatchingResources(locationPattern);}else {// 所有具有给定名称的类路径资源return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));}}else {// 通常只在后缀前加上前缀,// 并且在Tomcat中,只有在“war:”协议下,才会使用“*/”分隔符。int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :locationPattern.indexOf(':') + 1);if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {// 是一个文件模式return findPathMatchingResources(locationPattern);}else {// 具有给定名称的单个资源return new Resource[] {getResourceLoader().getResource(locationPattern)};}}
}
接着我们进入到findPathMatchingResources的方法中,具体源码如下:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {// 确定根目录路径String rootDirPath = determineRootDir(locationPattern);// 获取locationPattern中根目录路径之后的部分String subPattern = locationPattern.substring(rootDirPath.length());// 获取根目录资源Resource[] rootDirResources = getResources(rootDirPath);// 创建一个LinkedHashSet集合Set<Resource> result = new LinkedHashSet<>(16);// 遍历根目录资源for (Resource rootDirResource : rootDirResources) {// 解析根目录资源rootDirResource = resolveRootDirResource(rootDirResource);// 获取根目录资源的URLURL rootDirUrl = rootDirResource.getURL();// 如果存在equinoxResolveMethod且根目录URL的协议以"bundle"开头if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {// 调用equinoxResolveMethod方法,获取解析后的URLURL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);// 如果解析后的URL不为空if (resolvedUrl != null) {// 将解析后的URL赋值给rootDirUrlrootDirUrl = resolvedUrl;}// 将解析后的URL封装为ResourcerootDirResource = new UrlResource(rootDirUrl);}// 如果根目录URL的协议以"vfs"开头if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {// 调用VfsResourceMatchingDelegate的findMatchingResources方法,获取匹配的资源result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));}// 如果根目录URL是jar文件或者根目录资源是jar资源else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {// 调用doFindPathMatchingJarResources方法,获取匹配的jar资源result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));}// 否则else {// 调用doFindPathMatchingFileResources方法,获取匹配的文件资源result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));}}// 如果存在日志日志功能if (logger.isTraceEnabled()) {// 打印日志logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);}// 返回结果return result.toArray(new Resource[0]);
}
1.3 获取元数据
源码:可以看到通过1.2中获取到的资源将通过如下方法获取一个名为MetadataReader的对象,那么这个元数据读取器究竟是干什么的?
// getMetadataReaderFactory工厂模式构建MetadataReader
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
MetadataReader接口中只定义了三个方法,源码如下:
public interface MetadataReader {Resource getResource();ClassMetadata getClassMetadata();AnnotationMetadata getAnnotationMetadata();
}
根据debug追随到的源码,发现其进入到了MetadataReader的实现类SimpleMetadataReader中,其构造器如下:
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {// 这里使用到了经典的访问者模式,可以自行了解一下SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);getClassReader(resource).accept(visitor, PARSING_OPTIONS);this.resource = resource;this.annotationMetadata = visitor.getMetadata();
}
感兴趣的可以着重看下getClassReader(resource).accept(visitor, PARSING_OPTIONS);这句话中大概做了那些事情。
具体源码就不展示,这里大概就是将class文件读取并操作二进制代码。(可以通过《深入了解Java虚拟机》这本书来了解一下java的字节码相关内容)
1.4 创建ScannedGenericBeanDefinition
通过构造器的方式将MetadataReader中读取到的内容放入BeanDefinition中。
public ScannedGenericBeanDefinition(MetadataReader metadataReader) {Assert.notNull(metadataReader, "MetadataReader must not be null");this.metadata = metadataReader.getAnnotationMetadata();setBeanClassName(this.metadata.getClassName());setResource(metadataReader.getResource());
}
2. 获取Bean的名称
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
我们可以看一下spring是如何获取bean的名称的
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {if (definition instanceof AnnotatedBeanDefinition) {String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);if (StringUtils.hasText(beanName)) {// Explicit bean name found.return beanName;}}// Fallback: generate a unique default bean name.return buildDefaultBeanName(definition, registry);
}
可以看到该方法是有两个分支,如果通过注解中可以拿到名字,则直接返回,具体方法如下:
// 确定基于注解的bean名称
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {// 获取注解的元数据AnnotationMetadata amd = annotatedDef.getMetadata();// 获取注解的类型Set<String> types = amd.getAnnotationTypes();// 初始化bean名称String beanName = null;// 遍历注解类型for (String type : types) {// 获取注解的属性AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);if (attributes != null) {// 获取注解的元注解类型Set<String> metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> {Set<String> result = amd.getMetaAnnotationTypes(key);return (result.isEmpty() ? Collections.emptySet() : result);});// 判断注解是否为stereotype注解,并且包含name和value属性if (isStereotypeWithNameValue(type, metaTypes, attributes)) {// 获取name属性的值Object value = attributes.get("value");if (value instanceof String) {String strVal = (String) value;// 判断字符串长度是否大于0if (StringUtils.hasLength(strVal)) {// 判断beanName是否为空,或者与strVal不相等if (beanName != null && !strVal.equals(beanName)) {// 抛出异常throw new IllegalStateException("Stereotype annotations suggest inconsistent " +"component names: '" + beanName + "' versus '" + strVal + "'");}// 更新beanNamebeanName = strVal;}}}}}return beanName;
}
或者,根据类的class直接创建名称,代码如下:
protected String buildDefaultBeanName(BeanDefinition definition) {// 获取bean的类名String beanClassName = definition.getBeanClassName();// 断言bean的类名不能为空Assert.state(beanClassName != null, "No bean class name set");// 获取bean的简短名称String shortClassName = ClassUtils.getShortName(beanClassName);// 将简短名称的首字母转换为小写return Introspector.decapitalize(shortClassName);
}
3. 处理带有注解的BeanDefinition的公共定义
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
在此方法中,我们可以了解到,注册为Bean的类上可以添加的注解有几种,代码如下:
static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);if (lazy != null) {abd.setLazyInit(lazy.getBoolean("value"));}else if (abd.getMetadata() != metadata) {lazy = attributesFor(abd.getMetadata(), Lazy.class);if (lazy != null) {abd.setLazyInit(lazy.getBoolean("value"));}}if (metadata.isAnnotated(Primary.class.getName())) {abd.setPrimary(true);}AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);if (dependsOn != null) {abd.setDependsOn(dependsOn.getStringArray("value"));}AnnotationAttributes role = attributesFor(metadata, Role.class);if (role != null) {abd.setRole(role.getNumber("value").intValue());}AnnotationAttributes description = attributesFor(metadata, Description.class);if (description != null) {abd.setDescription(description.getString("value"));}
}
4 将定义好的BeanDefinition放入set,并返回set。
if (checkCandidate(beanName, candidate)) {// 创建一个BeanDefinitionHolder,并将其添加到集合中BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);// 应用代理模式definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);// 将BeanDefinition注册到容器中registerBeanDefinition(definitionHolder, this.registry);
}
思考总结:
1、可以看到,在spring中使用了很多设计模式,设计模式在解决一系列问题上非常有帮助,如创建Bean的Bean工厂,访问并根据字节码操作的访问者模式,需要理解其思想,在遇到问题的时候,往设计模式上想一想。后续可能要深入的学习一下设计模式。
2、关于数据类型的使用,其实本人在实际开发的过程中大部分时间都使用list,Map,其中也遇到过很多次内存溢出,在合理的选择数据类型上,有必要仔细斟酌。