写点什么

Spring Boot Devtools Restarter 原理

用户头像
a1vin-tian
关注
发布于: 2021 年 05 月 27 日

简介

Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The spring-boot-devtools module can be included in any project to provide additional development-time features. To include devtools support, add the module dependency to your build, as shown in the following listings for Maven and Gradle:


Maven


<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-devtools</artifactId>        <optional>true</optional>    </dependency></dependencies>
复制代码

用途

其核心功能就是可以在 DEBUG 的时候,可以在不重启进程的情况下重启spring context,从而省去每次都重启进程和加载公共包的时间。(其实没啥用,实际情况是大头时间都花费在初始化 bean 上)


官方也是这么说的Automatic Restart


Applications that use spring-boot-devtools automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application.

原理

这里主要讲Automatic Restart


As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using:In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart.In IntelliJ IDEA, building the project (Build +→+ Build Project) has the same effect.If using a build plugin, running mvn compile for Maven or gradle build for Gradle will trigger a restart.

热重启是怎么做的?

简单来讲就是项目启动的时候,项目启动的时候,Devtools中的RestartApplicationListener会拦截一些事件,然后将原本 spring 加载的处理流程接管到Restarter进行处理. 每次重启spring的时候,会使用一个全新的RestartClassLoader和线程调用 main 方法,以完成 spring 的重启。


为什么要使用新的RestartClassLoader呢,因为如果不加 javaagent 的话,java 的已加载 class 是不能重新定义的,只能通过新建 classloader 重新加载.

启动时:


class RestartApplicationListener {... @Override public void onApplicationEvent(ApplicationEvent event) { // 用于初始化initializer,调用Restarter.initialize() if (event instanceof ApplicationStartingEvent) { onApplicationStartingEvent((ApplicationStartingEvent) event); } if (event instanceof ApplicationPreparedEvent) { onApplicationPreparedEvent((ApplicationPreparedEvent) event); } if (event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) { Restarter.getInstance().finish(); } if (event instanceof ApplicationFailedEvent) { onApplicationFailedEvent((ApplicationFailedEvent) event); } } ... }
复制代码


onApplicationStartingEvent的 ApplicationStartingEvent 事件会调用该方法.


class Restarter {protected void initialize(boolean restartOnInitialize) {    preInitializeLeakyClasses();    if (this.initialUrls != null) {      this.urls.addAll(Arrays.asList(this.initialUrls));      if (restartOnInitialize) {        this.logger.debug("Immediately restarting application");        immediateRestart();      }    }  }
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(); } }
复制代码


创建新的 ClassLoader



class Restarter {
private Throwable doStart() throws Exception { Assert.notNull(this.mainClassName, "Unable to find the main class to restart"); URL[] urls = 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 relaunch(classLoader); }
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(); }}
复制代码


启动 main 方法,重新启动 spring,起到重启 spring 的作用.


class RestartLauncher {
@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); } }}
复制代码

运行时触发

运行时触发重启是通过 scan 文件的变化进行触发的. 具体实现如下:


启动的时候会定义ClassPathFileSystemWatcher,其委托了FileSystemWatcher进行文件的 scan,当发生变更的时候触发ClassPathChangedEvent,然后调用Restarter.restart重启 spring.扫描的路劲是由以下代码,基本上就是 class 目录. 当然我们可以在META-INF/spring-devtools.properties进行扩充.



private ChangeableUrls(URL... urls) { DevToolsSettings settings = DevToolsSettings.get(); List<URL> reloadableUrls = new ArrayList<>(urls.length); for (URL url : urls) { if ((settings.isRestartInclude(url) || isDirectoryUrl(url.toString())) && !settings.isRestartExclude(url)) { reloadableUrls.add(url); } } if (logger.isDebugEnabled()) { logger.debug("Matching URLs for reloading : " + reloadableUrls); } this.urls = Collections.unmodifiableList(reloadableUrls);}
复制代码


文件触发的 bean 定义


    @Bean    ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(        FileSystemWatcherFactory fileSystemWatcherFactory) {      return (event) -> {        if (event.isRestartRequired()) {          Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));        }      };    }        @Bean    @ConditionalOnMissingBean    ClassPathFileSystemWatcher classPathFileSystemWatcher(FileSystemWatcherFactory fileSystemWatcherFactory,        ClassPathRestartStrategy classPathRestartStrategy) {      URL[] urls = Restarter.getInstance().getInitialUrls();        ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(fileSystemWatcherFactory,          classPathRestartStrategy, urls);      watcher.setStopWatcherOnRestart(true);      return watcher;    }
复制代码


重启spring的代码,ClassPathFileSystemWatcher的代码就不贴了,就是间隔循扫描文件变化,代码比较简单.


public void restart(FailureHandler failureHandler) {    if (!this.enabled) {      this.logger.debug("Application restart is disabled");      return;    }    this.logger.debug("Restarting application");    getLeakSafeThread().call(() -> {      // close之前的context.       Restarter.this.stop();      // 跟启动步骤一样      Restarter.this.start(failureHandler);      return null;    });  }  protected void start(FailureHandler failureHandler) throws Exception {  // 重试重启spring直到成功或者是`Outcome.ABORT`  do {    Throwable error = doStart();    if (error == null) {      return;    }    if (failureHandler.handle(error) == Outcome.ABORT) {      return;    }  }  while (true);}
复制代码

总结

通过学习Spring Boot Devtools,我们可以学习几个知识点,包括扫描文件,和如何在运行时重新加载类. 具体的细节还需要读者深入看下,本文只是提供了知识的索引和原理的关键代码。这个在实际过程中感觉不太好用,还没有 IDE 的热加载类速度块. 且如果带到生产上还会带来一些不必要的麻烦(class not found).

参考

https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-devtools

发布于: 2021 年 05 月 27 日阅读数: 12
用户头像

a1vin-tian

关注

还未添加个人签名 2018.04.21 加入

还未添加个人简介

评论

发布
暂无评论
Spring Boot Devtools Restarter 原理