SpringBoot DevTools 是怎样完成应用热部署,节省你的开发时间的
前置内容:
类加载器( 类加载器与类冲突)
热部署 (类加载器与类的热替换(Hotswap))
SpringBoot Starter 工作原理(如何开发自己的Spring Boot Starter)
DCL (来看看你貌似熟悉的单例模式)
Java 程序员一定都或多或少听过老一辈讲他们的一些开发故事。其中用「一袋烟」的工夫等应用的重启,特别是在 EJB 还有市场的那几年。
而每次代码的修改导致的重启等待,都是我们加班的诱因。所以热部署,热加载等技术一直很受欢迎。
在SpringBoot 流行的今天,SpringBoot项目也提供了一些工具,让我们的应用开发生活能更美好,毕竟少等一会应用重启,就能干些别的「大」事。
其中,SpringBoot DevTools 就是这些工具中不得不提的。本次咱们重点来看下
DevTools 为我们提供的支持 Spring Boot应用热部署的功能,无需手动重启Spring Boot应用,可以极大提高开发效率。
如何使用
使用很方便,在应用的pom.xml
中增加依赖即可。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
既然是 「Dev」Tool,开发的也比较机智,只在开发环境用,生产环境就自动停用了。判断生产环境的方便是是否通过 java -jar
的方式启动,或者是「自定义的ClassLoader」。而且一般也推荐依赖的时候做为optional
。
然后在项目中修改你的应用代码,你就会发现应用自动重新部署了。但是分明比你自己重新启动要快好多。
怎么做到的,为什么快?
带着这个疑问,我们一起看下 DevTools的工作原理。
在 SpringBoot 的主类 启动 SpringApplication
时,会产生一系列的事件,通过观察者模式,进行 multicastEvent ,这一系列事件会广播到各个Listener。
在增加 DevTools依赖后,这些 Listener 中,会有一个RestartApplicationListener
。
它在事件通知时会干啥呢?
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
String enabled = System.getProperty("spring.devtools.restart.enabled");
if(enabled != null && !Boolean.parseBoolean(enabled)) {
Restarter.disable();
} else {
String[] args = event.getArgs();
DefaultRestartInitializer initializer = new DefaultRestartInitializer();
boolean restartOnInitialize = !AgentReloader.isActive();
Restarter.initialize(args, false, initializer, restartOnInitialize);
}
}
我们看到,这里会初始化一个Restarter
。初始化的时候,会使用到著名的「c双重锁检查」。
下面的代码,就是初始化时的 DCL。
public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer, boolean restartOnInitialize) {
Restarter localInstance = null;
Object var5 = INSTANCE_MONITOR;
synchronized(INSTANCE_MONITOR) {
if(instance == null) {
localInstance = new Restarter(Thread.currentThread(), args, forceReferenceCleanup, initializer);
instance = localInstance;
}
}
if(localInstance != null) {
localInstance.initialize(restartOnInitialize);
}
}
初始化时,会生成 Restarter的实例,我们看,此时会保存下来一些关键信息,像主类名称,参数等等,请留意,这些是后面重启的要素。
除参数外,还有一个名为LeakSafeThread
的重要「人物」以及加载类URL的分类。
protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup, RestartInitializer initializer) {
SilentExitExceptionHandler.setup(thread);
this.forceReferenceCleanup = forceReferenceCleanup;
this.initialUrls = initializer.getInitialUrls(thread);
this.mainClassName = this.getMainClassName(thread);
this.applicationClassLoader = thread.getContextClassLoader();
this.args = args;
this.exceptionHandler = thread.getUncaughtExceptionHandler();
this.leakSafeThreads.add(new Restarter.LeakSafeThread());
}
首先,类的加载我们都知道,是通过类加载完成的。类加载去哪里加载类呢?当然是在我们启动时指定的类路径上。为了完成应用的快速启动, DevTools的巧妙之外在于,这里将第三方类库的类和用户自定义的类做了区分。
这里的initialUrls
,就是做这个的,他会根据路径判断,将用户自定义类的URL选出来。
private ChangeableUrls(URL... urls) {
DevToolsSettings settings = DevToolsSettings.get();
List<URL> reloadableUrls = new ArrayList(urls.length);
URL[] var4 = urls;
int var5 = urls.length;
for(int var6 = 0; var6 < var5; ++var6) {
URL url = var4[var6];
if((settings.isRestartInclude(url) || this.isFolderUrl(url.toString())) && !settings.isRestartExclude(url)) {
reloadableUrls.add(url);
}
}
if(logger.isDebugEnabled()) {
logger.debug("Matching URLs for reloading : " + reloadableUrls);
}
this.urls = Collections.unmodifiableList(reloadableUrls);
}
这段代码是从整个项目的加载类路径中找到我们自己的类,也就是非第三方类库,因为这些第
三方的库是不会变的,我们所改的都是自己项目的内容。
然后这里呢,会添加这样一个Thread
this.leakSafeThreads.add(new Restarter.LeakSafeThread());
这个leakSafeThread是做啥的呢?
这是Restarter的一个内部类
private class LeakSafeThread extends Thread {
private Callable<?> callable;
private Object result;
LeakSafeThread() {
this.setDaemon(false);
}
public void call(Callable<?> callable) {
this.callable = callable;
this.start();
}
public <V> V callAndWait(Callable<V> callable) {
this.callable = callable;
this.start(); // 这里把线程自己给启动起来
try {
this.join();
return this.result;
} catch (InterruptedException var3) {
Thread.currentThread().interrupt();
throw new IllegalStateException(var3);
}
}
public void run() {
try {
Restarter.this.leakSafeThreads.put(Restarter.this.new LeakSafeThread());
this.result = this.callable.call();
} catch (Exception var2) {
var2.printStackTrace();
System.exit(1);
}
}
}
那这个线程是什么时候被启动的呢?看一眼前面的初始化内容
if(localInstance != null) {
localInstance.initialize(restartOnInitialize);
}
protected void initialize(boolean restartOnInitialize) {
this.preInitializeLeakyClasses();
if(this.initialUrls != null) {
this.urls.addAll(Arrays.asList(this.initialUrls));
if(restartOnInitialize) {
this.logger.debug("Immediately restarting application");
this.immediateRestart();
}
}
}
看,这就和LeakSafeThread串起来了。
private void immediateRestart() {
try {
this.getLeakSafeThread().callAndWait(() -> {
this.start(FailureHandler.NONE);
this.cleanupCaches();
return null;
});
} catch (Exception var2) {
}
}
protected void start(FailureHandler failureHandler) throws Exception {
Throwable error;
do {
error = this.doStart();
if(error == null) {
return;
}
} while(failureHandler.handle(error) != Outcome.ABORT);
}
private Throwable doStart() throws Exception {
URL[] urls = (URL[])this.urls.toArray(new URL[0]);
ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger);
return this.relaunch(classLoader);
}
重点来了,这里加入了一个新的人物:RestartClassLoader
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();
}
class RestartLauncher extends Thread {
private final String mainClassName;
private final String[] args;
private Throwable error;
RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args, UncaughtExceptionHandler exceptionHandler) {
this.mainClassName = mainClassName;
this.args = args;
this.setName("restartedMain");
this.setUncaughtExceptionHandler(exceptionHandler);
this.setDaemon(false);
this.setContextClassLoader(classLoader);
}
}
这也是个新的线程,我们看到前面Restarter里记下来了MainClass的名字,参数
这里搞了一个新的ClassLoader,然后后面我想你也猜到了
反射。
看一眼具体的调用栈:
"restartedMain@2014" prio=5 tid=0xe nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.springframework.boot.devtools.restart.classloader.RestartClassLoader.getResources(RestartClassLoader.java:93)
at org.springframework.core.io.support.SpringFactoriesLoader.loadSpringFactories(SpringFactoriesLoader.java:130)
at org.springframework.core.io.support.SpringFactoriesLoader.loadFactoryNames(SpringFactoriesLoader.java:119)
at org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:429)
at org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:421)
at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:268)
at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:249)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246)
at com.finecity.RabbitApplication.main(RabbitApplication.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
public void run() {
try {
Class<?> mainClass = this.getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", new Class[]{String[].class});
mainMethod.invoke((Object)null, new Object[]{this.args});
} catch (Throwable var3) {
整体的启动不久就完成了,和我们加 DevTools之前的区别是,这里的线程是restartedMain
,他已经接管了我们的启动。
重部署呢?
那我们对于代码的变更,又是怎样作用到Thread上的呢?修改一个自己的业务代码,你会发现,原来是本地启动了一个File Watcher,看看这个调用栈:
"File Watcher@7244" daemon prio=5 tid=0x1a nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.springframework.boot.devtools.restart.Restarter$LeakSafeThread.call(Restarter.java:612)
at org.springframework.boot.devtools.restart.Restarter.restart(Restarter.java:251)
at org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration$RestartConfiguration.onClassPathChanged(LocalDevToolsAutoConfiguration.java:108)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:261)
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:180)
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:142)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:400)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:354)
at org.springframework.boot.devtools.classpath.ClassPathFileChangeListener.publishEvent(ClassPathFileChangeListener.java:68)
at org.springframework.boot.devtools.classpath.ClassPathFileChangeListener.onChange(ClassPathFileChangeListener.java:64)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.fireListeners(FileSystemWatcher.java:309)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.updateSnapshots(FileSystemWatcher.java:302)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.scan(FileSystemWatcher.java:262)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.run(FileSystemWatcher.java:242)
at java.lang.Thread.run(Thread.java:748)
此时,会调用到Restarter的 restart方法上
public void restart(FailureHandler failureHandler) {
if(!this.enabled) {
this.logger.debug("Application restart is disabled");
} else {
this.logger.debug("Restarting application");
this.getLeakSafeThread().call(() -> {
this.stop(); // 第一步
this.start(failureHandler); // 第二步
return null;
});
}
}
具体的restart,是先stop
,再start
,start的过程又和我们前面启动时一样。
protected void stop() throws Exception {
this.logger.debug("Stopping application");
this.stopLock.lock();
try {
Iterator var1 = this.rootContexts.iterator();
while(true) {
if(!var1.hasNext()) {
this.cleanupCaches();
if(this.forceReferenceCleanup) {
this.forceReferenceCleanup();
}
break;
}
ConfigurableApplicationContext context = (ConfigurableApplicationContext)var1.next();
context.close();
this.rootContexts.remove(context);
}
} finally {
this.stopLock.unlock();
}
System.gc();
System.runFinalization();
}
private Throwable doStart() throws Exception {
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
URL[] urls = (URL[])this.urls.toArray(new URL[0]);
ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger);
if(this.logger.isDebugEnabled()) {
this.logger.debug("Starting application " + this.mainClassName + " with URLs " + Arrays.asList(urls));
}
return this.relaunch(classLoader);
}
总结
整体来说,DevTools能快速热部署,主要在于ClassLoader做了区分,一个加载第三方类,另一个称为RestartClassLoader的加载用户类,这样在有代码更改的时候,原来的ClassLoader 会被清除,进行gc,重新创建一个RestartClassLoader,由于需要加载的类相比较少,所以重启很快。
近期文章
如果你喜欢本文
请长按二维码关注
转发朋友圈,是对我最大的支持。
更多精彩内容:
一台机器上安装多个Tomcat 的原理(回复001)
监控Tomcat中的各种数据 (回复002)
启动Tomcat的安全机制(回复003)
乱码问题的原理及解决方式(回复007)
Tomcat 日志工作原理及配置(回复011)
web.xml 解析实现(回复 012)
线程池的原理( 回复 014)
Tomcat 的集群搭建原理与实现 (回复 015)
类加载器的原理 (回复 016)
类找不到等问题 (回复 017)
代码的热替换实现(回复 018)
Tomcat 进程自动退出问题 (回复 019)
为什么总是返回404? (回复 020)
...
PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看
如有帮助请点好看☟