【干货点】 此处是 【好好面试】 系列文的第13篇文章。 现在网上大把说IoC的文章,可是手把手调试源码的却不多,也很少会描述IoC容器的实现步骤,现在就让稀饭下雪手把手调试源码看看IoC是怎么个东西,顺便记住几个关键知识点,以后面试无忧;不懂如何看源码的兄弟们要跟上啦,可以仔细看看我的调试骚法。
必读大前提
依稀记得,在很多年前,我在简历技能栏目上写着熟悉spring几个字,后来面试官瞄了一眼,熟悉spring?不错,说说看IoC容器的实现。
我当时就懵逼了,说实话,我一直都知道IoC是什么,原理是什么,我还可以用类比的方式和你说这个鬼东西,可是你让我说说IoC容器的实现,我还真TM一时回答不上来,因为我没有看过源码。而这也是我写这篇文章的初衷,为了能够说清楚这个过程,我将整个SpringBoot的启动过程和IoC容器的实现过程都仔仔细细看了一遍。
看源码后的总结
仔细浏览了源码后,得出了一个总结,同时也是**【面试热点】** ,IoC容器的实现可以分为以下几步:
- 1、Resource的定位
使用过SpringBoot的都知道,关于组件的扫描是从主类所在的包开始扫描的,通过阅读源码我们可以看到,在prepareContext()方法中,SpringBoot会先将主类,也就是使用了注解@SpringBootApplication的类,解析成BeanDefinition,之后在invokeBeanFactoryPostProcessors()方法中解析主类的BeanDefinition从而获取basePackage的路径,而这个路径也就是我们所说的定位了,当然了,此处不讨论使用了注解@Import的情况。
- 2、BeanDefinition的载入
在取得了Resource的定位后,自然而然下一步肯定是将各种BeanDefinition的载入到内存中了,这里看源码其实就调试了很久,但是其实流程极其简单。所谓的载入内存中,用脚指头想想都知道其实就是通过Resource的定位,拼接了一下路径变成:classpath*:org/springframework/boot/demo/**/*.class这样的形式,之后再使用了一个比较屌的类PathMatchingResourcePatternResolver,将该路径下的所有.class的文件加载进来,之后再遍历判断是否有@Component注解,不过不要以为可以直接通过@Component注解来反调源码,年轻的我也是这么做的,不过发现其实不是哟,想知道就继续看。如果有注解的话,就是该步骤要装载的BeanDefinition了。
- 3、注册BeanDefinition
所谓的注册,听起来很高大上,将类注册入IoC容器内,但是其实很简单,其实就是将其put到一个CurrentHashMap中罢了,是的,IoC容器其实就是使用这个Map来存放这些BeanDefinition数据的。
【温馨提示】:如果不知道BeanDefinition是什么的同学就该面壁了哟,说明Spring了解确实浅,同时也没有看我之前的文章,可以看看这篇Spring之Bean的动态注册
其实看到这里也差不多可以了,毕竟大致描述下过程的话确实就是上面三个了,有兴趣看源码解析的可以继续。后面我会将调试的流程跟着源码描述出来,最好是跟着代码调试,不然会晕圈的。
资源的定位
依旧按照之前套路,直接先定位到资源定位的地方,再通过调试页面,看看程序做了什么,我们可以直接跳到 ComponentScanAnnotationParser类中的parse函数,首先先解释一波什么是ComponentScanAnnotationParser ,所谓的ComponentScanAnnotationParser其实只是Spring的一个内部工具,它会基于某个类上的 @ComponentScan 注解属性分析指定包(package)以获取其中的BeanDefiniiton。
接下来我们看看其中的parse函数,跟着调试的同学可以直接给这个函数的最后一段代码加个、断点,启动项目,就可以看到项目的运行流程了,如下所示
// 通过调试可以看到componentScan携带者basePackages这个数据,而declaringClass是
// "com.nuofankj.demo.DemoApplication",也就是包路径+主类
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));
ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}
scanner.setResourcePattern(componentScan.getString("resourcePattern"));
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}
boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}
// 【重点】真正存放资源位置的地方
Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
// 【重点】该次运行,由于没有是默认启动,因此最终basePackages存放的是declaringClass的包路径,这点可以直接看
// ClassUtils.getPackageName(declaringClass)ll
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});
// 【重点】StringUtils.toStringArray(basePackages) 断点可以看到打印出来的是主类的包名【com.nuofankj.demo】
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
接下来让我们看看 scanner.doScan 中做了什么
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 【重点】从指定的包中扫描需要装载的Bean
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
// 【重点】将该 Bean 注册进 IoC容器(beanDefinitionMap)
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
上面中有标注了两个比较重要的方法
Set candidates = findCandidateComponents(basePackage); 作用是从basePackage中扫描类并解析成BeanDefinition,邓拿到所有符合条件的类后在 **registerBeanDefinition(definitionHolder, this.registry);**中将该类注册进IoC容器。也就是说在这个方法中完成了IoC容器初始化过程的第二三步,BeanDefinition的载入,和BeanDefinition的注册。
到这里,资源的定位到这里就结束了。
BeanDefinition的载入
我们接着上面所说的函数scanCandidateComponents
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
// 【重要】拼接扫描路径,最终生成的是 classpath*:com/nuofankj/demo/**/*.class
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
// 从上面拼接出来的packageSearchPath 路径中扫描所有的类
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 {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// 【重要】这里判断该类是不是 @Component 注解标注的类
if (isCandidateComponent(metadataReader)) {
// 将该类封装成ScannedGenericBeanDefinition,这是BeanDefinition接口的实现类
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
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 {
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;
}
看源码和注释我们可以看到,packageSearchPath 是通过拼接出来的,而会在getResources(packageSearchPath)方法中扫描到了该路径下的所有的类,之后遍历这些Resources,判断是否是@Component 注解标注的类,并且不是需要排除掉的类。之后便将扫描到的类,解析成ScannedGenericBeanDefinition,该类是BeanDefinition接口的实现类。
到这里 IoC容器的BeanDefinition载入就结束了。
BeanDefinition的注册
载入结束了,接下来让我们看看如何进行BeanDefinition的注册,重新回到 registerBeanDefinition(definitionHolder, this.registry) ,根据调试可以看到,最终会进入以下函数
public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
String beanName = definitionHolder.getBeanName();
// 【重点】
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
String[] var4 = aliases;
int var5 = aliases.length;
for(int var6 = 0; var6 < var5; ++var6) {
String alias = var4[var6];
registry.registerAlias(beanName, alias);
}
}
}
我们先看看什么是BeanDefinitionRegistry,该类的作用主要是向注册表中注册 BeanDefinition 实例,完成 注册的过程。继续看下重点函数 registerBeanDefinition
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition) beanDefinition).validate();
} catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
// 【重点】可以看到这里,采用ConcurrentHashMap存放bean,看到这里便可以知道了,其实IoC容器的最底层就是一个ConcurrentHashMap,只是它被放到了某个对象中,通过看源码可以知道这个对象是DefaultListableBeanFactory
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
// 【重点】如果该类不允许 Overriding 直接抛出异常
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
"': There is already [" + existingDefinition + "] bound.");
} else if (existingDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isWarnEnabled()) {
logger.warn("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
} else if (!beanDefinition.equals(existingDefinition)) {
if (logger.isInfoEnabled()) {
logger.info("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
// 【重点】注册进beanDefinitionMap
this.beanDefinitionMap.put(beanName, beanDefinition);
} else {
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
} else {
// 【重点】如果仍处于启动注册阶段,注册进beanDefinitionMap
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}
通过一步步调试,我们可以看到最终走到了DefaultListableBeanFactory,首先介绍下什么是DefaultListableBeanFactory,该类可以说就是IoC容器本器了,调试源码看到最后,其实IoC容器就是一个ConcurrentHashMap。
那么DefaultListableBeanFactory是什么时候构建的呢?我们可以看到
通过obtainFreshBeanFactory构建的,内部函数调试后看了下没什么,不影响整体流程,就不深入讲解了,有兴趣可以自己跟着调试看看。
吹水几分钟
文章总结的我都调到前面了,所以总结就不说了,说说看最近更文速度变慢的原因,主要还是状态不好,忙是一直都忙的,毕竟身处游戏行业,每天9点或者10点上班,23点左右才下班,不过状态不好的原因主要还是情感问题,所以无心学习导致无心写文章。其次是发现最近容易脖子酸,职业病呀,关注我的应该大部分是程序员,如果有好的建议,可以提下哈,我说的是职业病预防,当然了,如果你要给我介绍女朋友我也是不介意的 ○( ^皿^)っHiahiahia…