阅读SpringBoot源码的心得记录
博主原创文章,转载请说明出处,但是不说我也管不了你🏳️
前言
前几天在写SpringBoot启动类的时候,本来是要在启动类上加@MapperScan
注解的,结果写成了@ComponentScan
注解,随后在启动SpringBoot项目的时候发现所有的Bean都没有加载到容器里,当时也是纳闷了一小会儿才发现是注解写错了,导致@ComponentScan
注解与@SpringBootApplication
注解冲突了,导致@SpringBootApplication
默认的包扫描全部失效了,所以容器才没法加载Bean。
之后我也是好奇为啥会这样,想知道@ComponentScan
与@SpringBootApplication
的关系以及@SpringBootApplication
默认的包扫描到底是啥样的,于是我便看了看注解的相关源码,一下子就想明白了,一边感慨规范的代码看起来确实爽(比吃完泻药都畅通),一边也觉得学会阅读他人的源码或者文档是一件很重要的事,特别是一些小众的项目,网上相关前辈的经验寥寥无几,只能依靠自己去一点一点阅读官方文档甚至源码,所以在此记录一下我解决问题的过程。
本文将以@ComponentScan
与@SpringBootApplication
冲突的问题为基础,详细记录一个小白应该如何通过源码阅读来理清这个问题的根本原因。
(其实我现在相关的阅读能力也很弱,特别是英文文档,虽然一直在训练自己英文文档阅读能力,但是仍然还是无法像阅读中文文档那样有”回到家“的感觉,有的时候读一个长难句就像便秘一样难受,服了。)
正文
1. 问题引出
1.1 问题描述
源码如下所示:
1 |
|
项目目录结构如下:
随后启动SpringBoot项目,但是通过规定地址访问项目内容时却均返回404 not found
。
1.2 问题排查与解决
经过反复确认访问地址没有问题后,再查看SpringBoot自己的日志打印,发现啥也没有,也就是说明代码和环境也没有问题,那么就可能是自己的逻辑写错了。而返回404的内容又是This application has no explicit mapping for /error
,遂推断可能是Controller的Bean根本就没有被加载到容器里,才导致地址没有映射,故开始验证猜测。使用idea自带的工具查看Spring容器里的Bean对象:
发现只有一个自己写Main被加载了,所以猜测是对的。而Bean没被加载一般都是包没有扫描的问题,而我只在Main类上写了扫描包的注解,最后问题解决:@MapperScan
注解错写成了@ComponentScan
注解。
2. 问题深究
所以为什么加了@ComponentScan
会使得@SpringBootApplication
扫描包的功能失效呢?只能猜测可能@SpringBootApplication
优先级更低,有了@ComponentScan
后@SpringBootApplication
就被覆盖掉了,而为了验证这个猜想可以通过阅读官方文档或者源码搞懂@SpringBootApplication
注解到底干了啥来解决这个疑问。
2.1 源码阅读
首先得强调一点,许多问题其实根本不需要我们去阅读源码特别是像SpringBoot这种体量大、社区活跃度高、使用量也是多得离谱的项目。因为第一,SpringBoot也算是比较复杂的项目了,有的部分靠自己去看源码可能得花大量时间;第二,这种很活跃的项目其实有很多的讲解教程,看大佬们的讲解比我们自己搁那慢慢研究效率会快很多且能学得更深刻。所以看这种类型项目的源码更多其实也是为了学习源码的规范或者是强化自己的代码阅读能力又或者是为了面试(doge)。下面开始进入正文。
既然是阅读源码,肯定需要一个阅读源码的工具(如果现在还有狠人用记事本看代码敲代码的,那确实是牛逼),而idea无疑是一个很强大的源码阅读工具,各种继承关系、调用关系都可以通过idea看得很清楚。本文也是使用的idea。
Ctrl+鼠标左键
查看@SpringBootApplication
的注解源码声明(为节省篇幅,部分代码不重要的内容会省略后同):
1 |
|
注释翻译:指明一个配置类,该类声明一个或多个@Bean
方法并触发自动配置和组件扫描。这是一个方便的注释,相当于声明@Configuration
、@EnableAutoConfiguration
和@ComponentScan
。
当然,从代码中可以看出,实现@SpringBootApplication
功能的关键注解是@ComponentScan
、@SpringBootConfiguration
、@EnableAutoConfiguration
。而这里的@ComponentScan
就只是为了让两个重要的类不被扫描,故只考虑另外两个到底是干嘛的。
先看@SpringBootConfiguration
:
1 |
|
可以从注释中看出,这个注解其实就是用来代替@Configuration
注解,套了这么多层娃,结果其实就是为了改一个名字,虽然可读性变好了,但是捏,不好说。
再来看@EnableAutoConfiguration
:
1 |
|
一来就看到个关键字:@AutoConfigurationPackage
里面的package,到这一步也基本上就确定这个注解就是用来自动配置包扫描的注解了。但是从注释的这句话:It is generally recommended that you place @EnableAutoConfiguration (if you're not using @SpringBootApplication) in a root package so that all sub-packages and classes can be searched.
意为:通常建议您将@EnableAutoConfiguration
(如果您没有使用@SpringBootApplication
)放在根包中,以便可以搜索所有子包和类。可以看出这个注解可以让配置类所在包下的子包以及类加入被扫描的队列。
再看@AutoConfigurationPackage
这个注解:
1 |
|
注释中的一句话:When no base packages or base package classes are specified, the package of the annotated class is registered.
,翻译出来就是:当基包或者基类没有被指定时,包中被用此注解的类就会被注册生效。基包或者基类就是指要被容器扫描注册的包或者类。
所以总算找到了答案:只要使用了@ComponentScan
注解并加上参数,则就指定了基包或基类,这样会导致@AutoConfigurationPackage
不会生效,也就是优先级问题。不过还是好奇是怎么实现包扫描的,从上面可以看出最终的功能实现是靠AutoConfigurationPackages.Registrar.class
这个类。
再看AutoConfigurationPackages.Registrar.class
:
1 |
|
注释写得很明白:该类用于存导入配置中的基包值。而从类中的方法里可以看到实际是PackageImports
这个类负责获取基包名。
再看PackageImports
:
1 |
|
重点关注其构造方法,可以看到构造方法里先是尝试从@AutoConfigurationPackage
注解的元信息中获取基包值,并作了个判断,如果没有获取到就使用注解所在包作为基包。至于怎么判断@ComponentScan
是否存在的具体方法,经过查找应该是在AutoConfigurationImportSelector
类中定义的,不过我也不想看了,因为各种类名、方法名太长了,看得我眼睛疼,意思意思就行了。
3. 总结
从以上可以看出,如果你头铁直接去读源码,其实也没多少技巧,就是靠注释再结合各种类名、方法名去理解甚至去猜测源码的具体意思,如果遇到一些写得烂的代码,这个时候估计得开始咒作者全家了,因为基本上只能去嗯猜,全靠自己的经验。其实SpringBoot的源码在我的角度也只是一些关键的地方注释写得非常清楚明白,但是在一些不重要的方法或者类上也是看得人脑壳疼,一些类名或者方法名真的是又臭又长,比如:getAutoConfigurationImportListeners
这种,来一两个还好,但是满屏都是这种是真的英语非母语者不友好,看得人眼睛是真的疼,虽然表达的意思清晰明了,但是信息密度太低了,来张图感受一下:
以上缺点我感觉也无解,要么就用缩写,更让人崩溃。不过好在每个方法代码长度都很短,很符合Robert C. Martin
那老计说的“代码整洁艺术”,只要代码长度短,写得再烂也容易读懂。
所以,放弃头铁,不要动不动就去读源码,有这精力去研究官方文档或者各路为爱发电的大神出的各种博客文章,早出师了,比读源码强一万倍,除非要去学习代码的各种命名规范、继承关系的逻辑等等,否则源码?狗都不看!
好困,写不动了,睡觉。