2024年最新Spring IoC 源码分析 (基于注解) (二) 之 包扫描,阿里面试java准备

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

BAT面试经验

实战系列:Spring全家桶+Redis等

其他相关的电子书:源码+调优

面试真题:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

//该构造函数会自动扫描以给定的包及其子包下的所有类,并自动识别所有的Spring Bean,将其注册到容器中

public AnnotationConfigApplicationContext(String… basePackages) {

//初始化

this();

//扫描包、注册bean

scan(basePackages);

refresh();

}

上文我们分析了this()方法,会去初始化AnnotatedBeanDefinitionReader读取器和ClassPathBeanDefinitionScanner扫描器,并初始化扫描过滤规则。

接下来我们看一下scan(basePackages)方法:

一直跟踪下去,发现调用了ClassPathBeanDefinitionScanner类中的scan()方法

//调用类路径Bean定义扫描器入口方法

public int scan(String… basePackages) {

//获取容器中已经注册的Bean个数

int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

//启动扫描器扫描给定包

doScan(basePackages);

// Register annotation config processors, if necessary.

//注册注解配置(Annotation config)处理器

if (this.includeAnnotationConfig) {

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

}

//返回注册的Bean个数

return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);

}

可以看到主要是doScan(basePackages)方法实现了扫描的逻辑,我们继续跟踪进去看下

//类路径Bean定义扫描器扫描给定包及其子包

protected Set doScan(String… basePackages) {

Assert.notEmpty(basePackages, “At least one base package must be specified”);

//创建一个集合,存放扫描到Bean定义的封装类

Set beanDefinitions = new LinkedHashSet<>();

//遍历扫描所有给定的包

for (String basePackage : basePackages) {

//调用父类ClassPathScanningCandidateComponentProvider的方法

//扫描给定类路径,获取符合条件的Bean定义

Set candidates = findCandidateComponents(basePackage);

//遍历扫描到的Bean

for (BeanDefinition candidate : candidates) {

//获取@Scope注解的值,即获取Bean的作用域

ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);

//为Bean设置作用域

candidate.setScope(scopeMetadata.getScopeName());

//为Bean生成名称

String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

//如果扫描到的Bean不是Spring的注解Bean,则为Bean设置默认值,

//设置Bean的自动依赖注入装配属性等

if (candidate instanceof AbstractBeanDefinition) {

postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);

}

//如果扫描到的Bean是Spring的注解Bean,则处理其通用的Spring注解

if (candidate instanceof AnnotatedBeanDefinition) {

//处理注解Bean中通用的注解,在分析注解Bean定义类读取器时已经分析过

AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);

}

//根据Bean名称检查指定的Bean是否需要在容器中注册,或者在容器中冲突

if (checkCandidate(beanName, candidate)) {

BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);

//根据注解中配置的作用域,为Bean应用相应的代理模式

definitionHolder =

AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

beanDefinitions.add(definitionHolder);

//向容器注册扫描到的Bean

registerBeanDefinition(definitionHolder, this.registry);

}

}

}

return beanDefinitions;

}

这一大段代码基本上就是spring扫描识别注解,并注册Bean到IOC容器中的代码。

在第10行有一个findCandidateComponents(basePackage)方法,这个方法里就是具体的扫描逻辑。

继续跟踪:

ClassPathScanningCandidateComponentProvider类

//扫描给定类路径的包

public Set findCandidateComponents(String basePackage) {

//spring5.0开始 索引 开启的话生成文件META-INF/spring.components 后面加载直接从本地文件读取(一般不建议开启 spring.index.ignore=true)

if (this.componentsIndex != null && indexSupportsIncludeFilters()) {

return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);

}

else {

return scanCandidateComponents(basePackage);

}

}

这里有一个if判断,我们默认走的是else里的分支,即scanCandidateComponents(basePackage)方法。

private Set scanCandidateComponents(String basePackage) {

Set candidates = new LinkedHashSet<>();

try {

//补全扫描路径,扫描所有.class文件 classpath*:com/mydemo/**/*.class

String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +

resolveBasePackage(basePackage) + ‘/’ + this.resourcePattern;

//定位资源

Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

boolean traceEnabled = logger.isTraceEnabled();

boolean debugEnabled = logger.isDebugEnabled();

for (Resource resource : resources) {

if (traceEnabled) {

logger.trace("Scanning " + resource);

}

if (resource.isReadable()) {

try {

//通过ASM获取class元数据,并封装在MetadataReader元数据读取器中

MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);

//判断该类是否符合@CompoentScan的过滤规则

//过滤匹配排除excludeFilters排除过滤器(可以没有),包含includeFilter中的包含过滤器(至少包含一个)。

if (isCandidateComponent(metadataReader)) {

//把元数据转化为 BeanDefinition

ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);

sbd.setResource(resource);

sbd.setSource(resource);

//判断是否是合格的bean定义

if (isCandidateComponent(sbd)) {

if (debugEnabled) {

logger.debug("Identified candidate component class: " + resource);

}

//加入到集合中

candidates.add(sbd);

}

else {

//不合格 不是顶级类、具体类

if (debugEnabled) {

logger.debug("Ignored because not a concrete top-level class: " + resource);

}

}

}

else {

//不符@CompoentScan过滤规则

if (traceEnabled) {

logger.trace("Ignored because not matching any filter: " + resource);

}

}

}

catch (Throwable ex) {

throw new BeanDefinitionStoreException(

"Failed to read candidate component class: " + resource, ex);

}

}

else {

if (traceEnabled) {

logger.trace("Ignored because not readable: " + resource);

}

}

}

}

catch (IOException ex) {

throw new BeanDefinitionStoreException(“I/O failure during classpath scanning”, ex);

}

return candidates;

}

这里就是主要的扫描逻辑,代码中的注释已经说的很清楚了。

主要过程:

  • 根据包路径,扫描所有.class文件

  • 根据包路径,生成.class对应的Resource对象

  • 通过ASM获取class元数据,并封装在MetadataReader元数据读取器中

  • 判断该类是否符合过滤规则

  • 判断该类是否为独立的类、具体的类

  • 加入到集合中

我们来详细看下过滤的方法 isCandidateComponent(metadataReader)

//判断元信息读取器读取的类是否符合容器定义的注解过滤规则

//@CompoentScan的过滤规则支持5种 (注解、类、正则、aop、自定义)

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {

//如果读取的类的注解在排除注解过滤规则中,返回false

for (TypeFilter tf : this.excludeFilters) {

if (tf.match(metadataReader, getMetadataReaderFactory())) {

return false;

}

}

//如果读取的类的注解在包含的注解的过滤规则中,则返回ture

for (TypeFilter tf : this.includeFilters) {

//判断当前类的注解是否match规则

if (tf.match(metadataReader, getMetadataReaderFactory())) {

//是否有@Conditional注解,进行相关处理

return isConditionMatch(metadataReader);

写在最后

可能有人会问我为什么愿意去花时间帮助大家实现求职梦想,因为我一直坚信时间是可以复制的。我牺牲了自己的大概十个小时写了这片文章,换来的是成千上万的求职者节约几天甚至几周时间浪费在无用的资源上。

复习一周,字节跳动三场技术面+HR面,不小心拿了offer

复习一周,字节跳动三场技术面+HR面,不小心拿了offer

上面的这些(算法与数据结构)+(Java多线程学习手册)+(计算机网络顶级教程)等学习资源

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

可能有人会问我为什么愿意去花时间帮助大家实现求职梦想,因为我一直坚信时间是可以复制的。我牺牲了自己的大概十个小时写了这片文章,换来的是成千上万的求职者节约几天甚至几周时间浪费在无用的资源上。

[外链图片转存中…(img-scqwMn7H-1715224844014)]

[外链图片转存中…(img-HcKAVEmX-1715224844014)]

上面的这些(算法与数据结构)+(Java多线程学习手册)+(计算机网络顶级教程)等学习资源

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值