Springboot-devtools原理分析

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阻塞。
调用堆栈如下:
调用堆栈.jpg

接下来,我们来看看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()

关于System.runFinalization和System.gc

清除软引用和弱引用的方法

/**
 * 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());
			}
		}
		if (resolve) {
			resolveClass(loadedClass);
		}
		return loadedClass;
	}
}
  1. 如果删除了某个类文件,那么直接抛出异常,不支持该种情况重启
  2. 检查当前类加载器是否已经加载过目标类,事实上这个一直返回false,因为每次重启都会new一个RestartClassLoader,尽管扫描出了被修改的文件,但是还是会去重新加载类路径下的所有文件,因为缓存之类的需要重建
  3. 调用自身的findClass方法,实际上和URLClassLoader的findClass差不多,这就先去指定路径下找
  4. 如果URL路径下没有找到,尝试Class.forName加载该类,此时使用的就是当前类的类加载器,即RestartClassLoader的类加载器AppClassLoader,引入的jar包就会在这里被加载。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值