Spring-Boot-Devtools 热部署源码详解 与 双亲委派机制(单步源码级分析)

这里默认大家有spring.factory,spring ioc等基本知识,不熟悉的同学可以去B站直接搜SpringBoot源码就能搜到视频,补充基础知识,这里带大家看SpringBoot的作者Phillip Webb大神是如何使用Spring-Dev-Tool工具实现基于事件的启动方式、自定义类加载器和Spring热部署的

**

基于事件的重启动方式

**

**

想必大家对SpingBoot的run方法非常的熟悉了,但各位有没有发现,如果自己DEBUG源码,如果引入了dev-tool依赖,有一个地方会莫名的断开,程序直接往下走呢?

**

在这里插入图片描述

仔细研究会发现,这两步其实已经不是同一个线程了:

图一,线程名称为“main”
在这里插入图片描述
图二,线程名称为"restartedMain"
在这里插入图片描述

其实到这里相信一些初学java的读者还是蒙的,一开始我甚至以为是ide有问题,为什么A线程(用户开启的"main"线程)会凭空消失,而B线程又会凭空的在下一步出现呢?接下来我们不妨进入这个starting()方法看看Phillip Webb大神是怎么写的

直接进入listeners.starting()方法,其实这里的循环只有一个元素,就是这个EventPublishRunListener方法,并且调用了它的starting()方法:
在这里插入图片描述
继续跟踪,发现只是把外面的application单例对象传了进去:
在这里插入图片描述
进入方法继续往下走,发现这里这里开始有多线程的影子了,这里其实就是用了监听器的思想,先传入一个事件,然后拿到所有这个事件相匹配的监听器,并执行监听器对应的回调方法,如果配置了线程池,就用多线程运行,但这里我没配,所以还是以单线程的方式继续走:
在这里插入图片描述
然后执行listener里的回调方法并把event对象传进去:
在这里插入图片描述
这里还会再次根据事件类型(event)执行不同的listener回调方法呢,这里见名知意,applicationStartingEvent,显然就是Boot容器 正在 开启的事件,进去看一看:
在这里插入图片描述
最核心的地方来了,“万恶之源”的Restarter类,前面的内容总结其实就是一堆无意义的跳转,没认真看也没关系,这里Phillip Webb才开始真正的表演,F7跟进去:
在这里插入图片描述

然后到了这个方法,注意这里我用了加粗,看看给什么属性赋了值,我的debug进了哪个if块,这里发紫的instance是静态的成员变量而且还是应用类加载器加载的,也就意味着只要JVM没炸就可以一直保留,进入这个方法:

在这里插入图片描述
看到lambd表达式和call、start等字样了吧,是不是和前面的线程切换开始挂钩了,这里不进入callAndWait()方法了,里面就是抛出一个线程执行call方法,所以重点是 start(FailureHandler.NONE)方法是如何重写Callable接口的:
在这里插入图片描述
这里Spring Boot在新线程创建伊始就创建了自定义类加载器, 然后把这个类加载器继续往下一个方法relaunch()传,这里开始已经不是main线程了,而是新的线程所运行的callable方法的回调,但是有没有发现,**线程的命名和一开始的"restartedMain"对不上!**这个线程不是真正的业务线程:
在这里插入图片描述
这里好像又准备要开启一个线程,注意看给launcher对象构造器传入的参数没有一个是多余的,自定义构造器、main方法所有的类名、用户传入的自定义参数、异常记录器,然后调用start()方法开启线程,话不多说,直接看launcher的run方法:

在这里插入图片描述
进入run方法,终于,和开头的线程切换对上号了,在构造器中,为线程对象设置自定义类加载器,然后在run方法里,直接通过反射重新掉起"main"方法,这里我也惊呆了,重新执行"main"方法!有没有人有疑惑,这么搞不会形成死循环么?
在这里插入图片描述
重新回到main方法中,打好断点,观察这个时候的线程名字是不是变了:
在这里插入图片描述
前面的步骤一模一样,读取spring.factory和System.properties等环境变量,但是会有一个地方会不同,还记得我前面加粗的地方么?
在这里插入图片描述

字体又一次的加粗,看看这个"万恶之源"和第一次有什么不同?前面main线程到这里的时候由于instance线程是null,所以进入了if条件,调用initialize()方法重启Spring-Boot,重启后看似一切都一样,但是别忘了,instance对象是一个由应用类加载器(appClassLoader)加载的静态成员属性,只要JVM不炸,无论如何都不会被回收,所以不会重新执行,我为什么要强调是应用类加载器下面讲:

在这里插入图片描述
**

还有一个问题,也是最重要的问题,Phillip Webb大神为什么要搞"多此一举"的重启SpringBoot的操作呢?

**

还记得除了instance属性之外还有的第二个不同点么?就是类加载器的不同,一开始由main方法创建的线程,里面所使用的类加载器都是JVM默认提供的AppClassLoader,只要是由这个类加载器加载的Class对象,想回收元空间(MetaSpace,一说静态方法区)的Class对象几乎是不可能的,这就引出为什么要"多此一举"的通过反射重新运行main方法,看看Phillip Webb大神在Restartor类的注释下怎么说的:

在这里插入图片描述
**

Phillip Webb是这么解释的,这个类实现热部署的原理是把class对象分成两部分,第一部分的class是万年保持不变的顶端部分,例如第三方的jar包(其实就是jar包下spring.factory声明的类以及这些类所依赖的其他类)和spring boot本身(注意:这里的类是class对象,不是实体对象!千万不要混淆!实体对象只会在下面的ioc容器里而且也会一起被回收),底部是需要频繁修改的通过URL加载的类(其实就是我们自己写的业务类),两者之间,也就是Phillip Webb所说的"顶部类"和"底部类"是通过spring.factory这个文件来区分,至于是怎么区分的看下面

熟悉SpringBoot的同学都知道,在创建SpringBoot实例化的时候会扫描所有jar包下名为spring.factory的类名并且使用Class.forName加载,这个时候的自定义加载器RestarterClassLoader还没被创建且没被设定为线程默认类加载器,于是就被AppClassLoader加载,在重启的时候,线程被替换为restartedMain线程并设定了RestarterClassLoader为线程默认类加载器,但这个时候spring.factory里声明的类,以及他们的依赖类已经被AppClassLoader加载,并且AppClassLoader为RestartClassLoader的父加载器,所以永远也不会被JVM回收,因此JVM会根据双亲委派机制和全限定类名拒绝加载已加载过的类,从而节省了磁盘IO的时间。

最后还有人记得一开始的main线程和连名字都没有的Thread-02线程么?他们最后的下场是一个抛出了空异常到JVM,一个被System.exit(1)方法,都被无声的关闭了。。。

**

Spring-Boot-Devtools热部署源码详解

基于事件的启动方式和自定义类加载器为SpringBoot热部署提供了可能。其实看懂了上面就应该猜到Spring-Dev-Tool是如何工作了吧,但是这里有一点细节需要大家注意,上面的SpringBoot重启机制是基于SpringBoot监听Boot的启动事件发生的,而实时文件监听是基于Spring IOC内置的原生监听器机制触发的,虽然大家都是共用同一个ApplicationListener接口。而且在调用重启方法上,虽然都是基于Restarter类,但具体的方法肯定不会是同一个。

原理其实很简单,引入Spring-Boot-Devtools依赖以后,bean会抛出一个监控线程,名称叫做"File Watch"的线程,这个线程会周期性扫描src目录下的所有文件
在这里插入图片描述

文件监控类:
在这里插入图片描述
**源码走起~~~~~~~~~~**
在这里插入图片描述
进入LocalDevToolsAutoConfiguration的一个静态内部类RestartConfiguration中,发现往内置IOC容器中添加了ClassPathFileSystemWatcher对象和FileSystemWatcher对象,
在这里插入图片描述
直接跳到run方法看实现吧,这里的while在没有同步修改业务代码的时候是个死循环,并且会周期性扫描target目录监听class文件和其他文件目录是否被修改或新增删除,remainingScans记录扫描次数,通常情况下为-1,既一直监听
在这里插入图片描述
这里可能有点绕,通常没有修改业务代码的时候,isDifferent()方法返回false,跳出循环,返回上一个方法,当我们改修改了业务代码并且按CTRL+F9同步到target目录的时候,isDifferent方法返回true,继续循环,经过赋值后又会返回false,下面的if才是真正判断是否执行热部署逻辑的
在这里插入图片描述
在业务代码中更改class文件,并按ctrl + F9编译到target目录下
在这里插入图片描述

监听到Class文件被更改,准备发布热部署事件,changeSet集合放的就是被修改或增删的文件的File()对象
在这里插入图片描述
没啥好说的,调用publishEvent()方法发布事件呗,这里有个小插曲,上面的isRestartRequire()方法除了判断changeSet是否为空外,Phillip Webb还很贴心的在里面增加了是判断当前环境是否为JRebel插件环境的判断,如果同时使用了JRebel热部署工具和devtool,则不会进行热部署以免浪费时间
在这里插入图片描述
最后执行事件回调,开启新的线程重新执行main方法,和前面开启SpringBoot流程差不太多,最后附上如何判断文件是否被修改的代码截图

在这里插入图片描述

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值