Springboot-devtools原理分析
title: Springboot-devtools原理分析 date: 2022-01-04 10:51:57.436 updated: 2022-01-06 15:47:16.452 url: /archives/65 categories: - Spring tags: - Spring - Java - Springboot
Springboot-devtools原理分析
springdev-tools实现开发过程中,自动重启应用程序,提供了一定的方便。 使用的话,需要引入starter依赖,然后设置IDEA文件更新策略,我一般设置为切出IDEA时更新类和文件。devtools检测类路径下文件夹变化,然后通过反射调用主类的Main方法重启应用程序,实现热部署。
为什么写这篇文章
最近在学习虚拟机类加载器相关的知识,顺受拿着Springboot项目测了几行代码,然后就发现了令自己困惑的事情。 首先热部署是基于Java的类加载机制的,然后devtools的原理大概就是监控类路径下class文件的变化,然后重新加载类,通过反射调用Main方法,重新启动程序。 这篇文章已经讲得比较清楚了 devtools基本原理
public static void main(String[] args) {
logger.debug(Connection.class.getSimpleName() + " " + Connection.class.getClassLoader());
logger.debug(Main.class.getSimpleName() + " " + Main.class.getClassLoader());
SpringApplication application = new SpringApplication(Main.class);
application.setBannerMode(Banner.Mode.OFF);
application.addListeners(new StartListener());
application.run(args);
logger.info(Configuration.class.getSimpleName() + " " + Configuration.class.getClassLoader());
logger.info(Connection.class.getSimpleName() + " " + Connection.class.getClassLoader());
logger.info(ApplicationContext.class.getSimpleName() + " " + ApplicationContext.class.getClassLoader());
logger.info(Main.class.getSimpleName() + " " + Main.class.getClassLoader());
}
程序运行后,控制台信息
10:31:57.317 [main] DEBUG com.rufeng.boot.Main - Connection null
10:31:57.322 [main] DEBUG com.rufeng.boot.Main - Main sun.misc.Launcher$AppClassLoader@18b4aac2
10:31:57.578 [Thread-1] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@36c60b36
10:31:57.581 [restartedMain] DEBUG com.rufeng.boot.Main - Connection null
10:31:57.581 [restartedMain] DEBUG com.rufeng.boot.Main - Main org.springframework.boot.devtools.restart.classloader.RestartClassLoader@36c60b36
2022-01-05 11:02:09.021 INFO 11848 --- [ restartedMain] com.rufeng.boot.Main : Configuration sun.misc.Launcher$AppClassLoader@18b4aac2
2022-01-05 11:02:09.021 INFO 11848 --- [ restartedMain] com.rufeng.boot.Main : Connection null
2022-01-05 11:02:09.021 INFO 11848 --- [ restartedMain] com.rufeng.boot.Main : ApplicationContext sun.misc.Launcher$AppClassLoader@18b4aac2
2022-01-05 11:02:09.021 INFO 11848 --- [ restartedMain] com.rufeng.boot.Main : Main org.springframework.boot.devtools.restart.classloader.RestartClassLoader@6f61b662
注意到3个问题 - 程序刚启动,三个线程的debug信息,二次执行 - 在SpringApplication run方法后的代码只执行了一次 - 第二次启动后,Main类的类加载器变为spring的RestartClassLoader
有以下几点想法: - main线程执行到run方法里面,没有出来了,所以后面的代码没有执行,有两种可能 - 被强行终止了 - 一直被join - jar包、Java自带的类被AppClassLoader加载,工作目录下的类被RestartClassLoader加载,当然,这个也不是绝对的 - 类字节码变化,重启应用程序,需要做哪些事情,直接反射调用Main方法?缓存?
流程分析
springboot启动后,在run方法中的listeners.starting方法中,发布了ApplicationStartingEvent,然后RestartApplicationListener开始运行,整个重启过程从这里开始。
监听器哪来的
此处有一个疑问,这个监听器只有引了devtools包后才会有,并且到starting时,容器还没有刷新,所有的Bean还未被解析,那么,这个监听器哪来的?
了解springboot自动配置的朋友们应该会想到,来自META/INF下的spring.factories文件,如下所示:
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.devtools.restart.RestartApplicationListener,\
org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener
在该监听器的方法中,我们关注的是onApplicationStartingEvent方法,在该方法中判断了devtools是否enabled,然后开始进入Restarter,这个类是重启的关键类
if (restartInitializer != null) {
String[] args = event.getArgs();
boolean restartOnInitialize = !AgentReloader.isActive();
if (!restartOnInitialize) {
logger.info("Restart disabled due to an agent-based reloader being active");
Restarter.initialize(args, false, restartInitializer, restartOnInitialize);
}
重启的逻辑在initialize方法中,我们需要讨论的也多在这个类中
private static final Object INSTANCE_MONITOR = new Object();
private static final String[] NO_ARGS = {};
private static Restarter instance;
public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer,
boolean restartOnInitialize) {
Restarter localInstance = null;
synchronized (INSTANCE_MONITOR) {
if (instance == null) {
localInstance = new Restarter(Thread.currentThread(), args, forceReferenceCleanup, initializer);
instance = localInstance;
if (localInstance != null) {
localInstance.initialize(restartOnInitialize);
}
单例的写法
可以看到,Restarter是一个单例,起初看到spring这种单例的写法时,下意识地想到一个问题,没写volatile,类还没构造完毕就被拿去用了?
后来发现自己还是不够仔细,对
还没完全理解,这里我们只讨论spring-devtools的这种写法和双重校验锁。
懒汉式,线程安全,同步整个方法
这是spring-devtools的写法,这种写法是不会出问题了,任何个线程进入到该方法必须获取到锁,一旦有线程释放锁,同步代码块必定被执行过,那么单例一定初始化完成,后续的线程获取到锁之后也不会再去初始化单例对象。
实际上,再去获取单例的时候,99%以上的情况都是初始化好的,不需要进入同步块,但是这样写差不多锁住了整个方法,性能上存在缺陷。
双重校验锁
public class Singleton {
private static final Object INSTANCE_MONITOR = new Object();
private static volatile Singleton instance;
private Singleton() {
public static Singleton getInstance() {
if (instance == null) {
synchronized (INSTANCE_MONITOR) {
if (instance == null) {
instance = new Singleton();
return instance;
}
与上面不同的是,这里首先执行第一个if,不需要获取锁,那么任何一个线程进入都可以执行,如果已被初始化(绝大多数情况下),直接返回单例对象,不需要阻塞等待,性能上得到优化。
但是,注意到一个问题,instance = new Singleton6()这句代码不是原子性的,从Java字节码的角度来说(Java字节码对CPU来说也不一定是原子的)
0 getstatic #2 <com/rufeng/Singleton.instance : Lcom/rufeng/Singleton;>
3 ifnonnull 38 (+35)
6 getstatic #3 <com/rufeng/Singleton.INSTANCE_MONITOR : Ljava/lang/Object;>
9 dup
10 astore_0 # 将栈顶引用型数值存入第一个本地变量
11 monitorenter # 进入同步代码块
12 getstatic #2 <com/rufeng/Singleton.instance : Lcom/rufeng/Singleton;> # 获取指定类的静态域,并将其值压入栈顶
15 ifnonnull 28 (+13) # 不为null
18 new #4 <com/rufeng/Singleton> # 创建一个对象,并将其引用值压入栈顶
21 dup # 复制栈顶数值并将复制值压入栈顶
22 invokespecial #5 <com/rufeng/Singleton.<init> : ()V> # 执行构造方法
25 putstatic #2 <com/rufeng/Singleton.instance : Lcom/rufeng/Singleton;> 为指定类的静态域赋值
28 aload_0 # 将第一个本地引用型变量推至栈顶
29 monitorexit # 退出同步代码块
30 goto 38 (+8)
33 astore_1
34 aload_0
35 monitorexit
36 aload_1
37 athrow
38 getstatic #2 <com/rufeng/Singleton.instance : Lcom/rufeng/Singleton;>
41 areturn # 从当前方法返回对象引用
按照Java字节码的流程来说,instance = new Singleton6()这条指令有三个步骤: 1. new 创建一个对象,分配内存空间 2. 执行构造方法 3. 将instance指向该对象
顺序执行的情况下,返回的单例对象一定是初始化完成了的,但是,指令可能会存在 重排序 的情况。 倘若3在2之前执行,对于单线程来说,不影响结果,在并发的情况下,可能会出现返回还没完全初始化的对象。考虑下面的情况: 1. 线程1进入方法,instance为null,拿到锁,instance为null,开始new对象,执行指令1,指令3。 2. 线程2进入方法,instance不为null,不需要进入同步代码块,直接返回instance,此时的instacne对象还未被初始化。 3. 线程1执行指令3,返回instance。
这样的话,线程2拿到的instance是有问题的。 如果同步整个方法,就算指令被重排序了,也是不会出现这种问题的。
反射重启程序
private void immediateRestart() {
try {
getLeakSafeThread().callAndWait(() -> {
start(FailureHandler.NONE);
cleanupCaches();
return null;
catch (Exception ex) {
this.logger.warn("Unable to initialize restarter", ex);
SilentExitExceptionHandler.exitCurrentThread();
}
当单例初始化完成之后,马上会执行immediateRestart方法,LeaksafeThread继承自Thread,其主要方法如下:
@SuppressWarnings("unchecked")
<V> V callAndWait(Callable<V> callable) {
this.callable = callable;
start();
try {
join();
return (V) this.result;
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
@Override
public void run() {
// We are safe to refresh the ActionThread (and indirectly call
// AccessController.getContext()) since our stack doesn't include the
// RestartClassLoader
try {
Restarter.this.leakSafeThreads.put(new LeakSafeThread());
this.result = this.callable.call();
}
我们来捋一下流程,此时仍然在main线程中,在main线程中,Restarter的initialize方法中,初始化Restarter单例,首次初始化完毕后,进入immediateRestart方法,获取LeakSafeThread线程对象,传入Callable对象,调用其callAndWait方法,该方法启动新线程,此时,两个线程开始了,也就是上图控制台第二个线程Thread-1。 启动后,调用LeakSafeThread的join方法,注意,仍然是在main线程中,此时重启线程正在运行,而main线程等待重启线程执行完,被join阻塞。 调用堆栈如下:
接下来,我们来看看LeakSafeThread的run方法,每次运行新的线程,就会往队列中put一个新的线程,然后真正执行Callable对象的方法。 即lambda Callable中的三行代码。然后继续到doStart方法,relaunch方法
protected Throwable relaunch(ClassLoader classLoader) throws Exception {
RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args,
this.exceptionHandler);
launcher.start();
launcher.join();
return launcher.getError();
}
RestartLauncher又是一个新的线程,注意,此时在LeakSafeThread线程中,线程名为Thread-1,又生出一个子线程,子线程运行后,join,等待子线程执行完成。
此时,main线程被join阻塞,等待Thread-1结束,Thread-1被join阻塞,等到RestarterLauncher结束,那么RestarterLauncher什么时候结束?下面是他的run方法:
@Override
public void run() {
try {
Class<?> mainClass = Class.forName(this.mainClassName, false, getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
catch (Throwable ex) {
this.error = ex;
getUncaughtExceptionHandler().uncaughtException(this, ex);
}
在其初始化方法中,setName("restartedMain"),及对应上文控制台debug的线程名,到这里,又回到了main方法。 此时main线程中的application还阻塞在run方法没有返回,重启的线程就会正常执行run方法启动应用程序,然后正常返回执行完main方法。该线程run方法结束。
该线程结束后,Thread-1不再阻塞,start方法结束,之后cleanupCaches()执行完,退出。
main线程如何静默退出
Thread-1退出,main线程不再阻塞,注意,main线程是在immediateRetart的satrt方法中阻塞,现在不再阻塞,继续immediateRestart方法,有一句比较关键的代码,而exitCurrentThread方法也只是简单地抛出了异常
SilentExitExceptionHandler.exitCurrentThread();
static void exitCurrentThread() {
throw new SilentExitException();
}
等等,抛了异常,怎么没报错呢? 结合控制台的输出,我们可以猜想,这句代码,把main线程终止了,而且是悄无声息的终止。
考虑以下代码
public static void main(String[] args) {
try {
new Thread(() -> {
int x = 1 / 0;
}).start();
} catch (Exception e) {
e.printStackTrace();
}
这样是没法捕获线程中抛出的异常的,除非在线程的run方法中捕获,无法在其他线程中处理线程发生的异常?答案是有的。 JDK提供了UncaughtExceptionHandler接口,用于处理多线程中发生的异常
public static void main(String[] args) {
try {
Thread thread = new Thread(() -> {
int x = 1 / 0;
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName());
System.out.println(e.getMessage());
thread.start();
} catch (Exception e) {
e.printStackTrace();
}
这样异常就会被捕获了,也就是控制台不会报错,并且main线程退出的原理了。 SilentExitExceptionHandler的处理异常的方法:
@Override
public void uncaughtException(Thread thread, Throwable exception) {
if (exception instanceof SilentExitException || (exception instanceof InvocationTargetException
&& ((InvocationTargetException) exception).getTargetException() instanceof SilentExitException)) {
if (isJvmExiting(thread)) {
preventNonZeroExitCode();
return;
if (this.delegate != null) {
this.delegate.uncaughtException(thread, exception);
}
上面分析的是程序启动后,马上又重启的过程,事实上这与检测到文件变化后再重启的流程有略微差别,立即重启多了退出main线程的部分,而检测文件变化重启多了事件监听、停止程序等工作。
文件变化监听
autoconfigure中,注入了FileSystemWatcher、ClassPathFileSystemWatcher、ApplicationListener三个关键的Bean
@Bean
ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(
FileSystemWatcherFactory fileSystemWatcherFactory) {
return (event) -> {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));
/* 监控文件变化的线程 */
/*org.springframework.boot.devtools.filewatch.FileSystemWatcher.Watcher*/
public void run() {
int remainingScans = this.remainingScans.get();
while (remainingScans > 0 || remainingScans == -1) {
try {
if (remainingScans > 0) {
this.remainingScans.decrementAndGet();
scan();
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
remainingScans = this.remainingScans.get();
}
线程不断扫描路径下的文件,一旦发生变化,即发布ClassPathChangedEvent,然后监听器调用Restater单例的restart方法完成重启。
类文件变化重启
restart方法,和immediateRestart很像,不同的是后者没有stop方法、同时线程没有退出
public void restart(FailureHandler failureHandler) {
if (!this.enabled) {
this.logger.debug("Application restart is disabled");
return;
this.logger.debug("Restarting application");
getLeakSafeThread().call(() -> {
Restarter.this.stop();
Restarter.this.start(failureHandler);
return null;
}
那么这里的流程也很清晰了,进入该方法时,所在的线程为Watcher线程,该线程生出子线程完成重启后,继续扫描工作,不需要退出。
stop清理工作
stop方法做了很多清理工作 - 关闭当前应用程序上下文,因为需要重新初始化 - 清除Class对象的有关缓存,比如ConversionService、RelectionUtils、AnnotationUtils等的缓存,因为类的字节码可能已被修改 - 如果需要的话,清除软引用和弱引用,方法是强制OOM - 执行一次GC - 执行System.runFinalization()
清除软引用和弱引用的方法
/**
* Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error.
private void forceReferenceCleanup() {
try {
final List<long[]> memory = new LinkedList<>();
while (true) {
memory.add(new long[102400]);
catch (OutOfMemoryError ex) {
// Expected
}
重新加载类文件
从上面反射调用main方法看到,使用的类加载器为RestartClassLoader,该类继承自URLClassLoader,表示支持从URL路径加载类字节码,如果所有的URL路径都找不到目标类的字节码文件,抛出ClassNotFoundException,其loadClass方法如下
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file != null && file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
synchronized (getClassLoadingLock(name)) {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
try {
loadedClass = findClass(name);
catch (ClassNotFoundException ex) {
loadedClass = Class.forName(name, false, getParent());