包扫描的过程
测试代码:
// 扫描指定包下的所有类
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()为true
if (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);
// 获取根目录资源的URL
URL rootDirUrl = rootDirResource.getURL();
// 如果存在equinoxResolveMethod且根目录URL的协议以"bundle"开头
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
// 调用equinoxResolveMethod方法,获取解析后的URL
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
// 如果解析后的URL不为空
if (resolvedUrl != null) {
// 将解析后的URL赋值给rootDirUrl
rootDirUrl = resolvedUrl;
}
// 将解析后的URL封装为Resource
rootDirResource = 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;
// 判断字符串长度是否大于0
if (StringUtils.hasLength(strVal)) {
// 判断beanName是否为空,或者与strVal不相等
if (beanName != null && !strVal.equals(beanName)) {
// 抛出异常
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
"component names: '" + beanName + "' versus '" + strVal + "'");
}
// 更新beanName
beanName = 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,其中也遇到过很多次内存溢出,在合理的选择数据类型上,有必要仔细斟酌。