1 引导语

在前面的几个小节中,我们主要围绕 SpringBoot 框架的相关核心功能点来讲解,主要分析了 SpringBoot 的设计初衷和相关核心设计,包括基于 main 方法来以独立 Java 进程的方式启动项目,SpringBoot 的自动配置机制、starter 包以及相关核心注解和 application.properties 属性配置文件等。

经过这些核心知识的讲解后,相信同学们都已经基本掌握了 SpringBoot 的用法,以及对相关功能点的设计与实现原理有了一个初步的认识。不过再进入实际开发之前,我还是想介绍一下 SpringBoot 的热部署的用法和实现原理,以便通过热部署来避免在以往的项目开发中存在的,每次代码修改都需要手动重启项目的问题,提高开发效率。

2 SpringBoot 热部署的概念与使用方法

2.1 热部署的概念

开发过前端应用的同学应该都知道,目前流行的前端框架,如 React 等,都是支持在每次对页面修改后,可以在无需重启的情况下,自动进行页面更新,这就是热部署的方式。对于 Java 服务端编程而言,Java 类文件的每次修改都需要重启服务器才能生效,这种方式一定程度影响了开发效率。

所以为了解决这个问题,实现与前端框架类似的热部署的功能,SpringBoot 在 spring-boot-devtools 开发工具包中实现了对类路径中文件进行修改之后,应用会自动重启,使得修改自动生效,而不再需要我们手动重启。

2.2 使用方法

SpringBoot 框架提供的 spring-boot-devtools 包是一种开发服务模块,只针对开发环境有效,要使用 SpringBoot 的热部署功能主要是围绕 spring-boot-devtools 包来展开的,并且需要对我们的开发工具 IDEA 进行一些配置,具体如下。

  1. 对 IDEA 进行配置,打开自动编译的开关,如下:点击左上角的 “Intellij IDEA -> Preferences” 按钮,然后弹出以下窗口,按照红色标注,依次点击 Build, Execution, Deployment -> Compiler,并勾选上"Build project automatically"。

java热部署实现方案_spring

然后对于 Mac 使用快捷键 shift + option + command + /,window 上的快捷键是 Shift + Ctrl + Alt + /,并在弹窗中选中 “registry”,最后在以下弹窗中勾选中"compiler.automake.allow.when.app.running":

java热部署实现方案_重启_02

  1. 完成对 IDEA 的配置之后,我们就可以对应用自身进行配置了。首先需要在 pom.xml 文件中添加 spring-boot-devtools 包的依赖,如下:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

Tips

spring-boot-devtools 实现的应用自动重启主要用在开发环境。如果是在生产环境,则该功能不会生效的,这也是为了保证生产环境的安全性。具体为通过 jar 包的方式启用应用时,如使用 java -jar 命令启动指定的 jar 包时,spring-boot-devtools 开发工具所提供的相关功能会失效。

  1. 通过以上配置之后,我们就可以使用 SpringBoot 的热部署功能了,是不是非常简单?不过我们继续来介绍一下该功能的更多特性,这些特性可以在 application.properties 文件进行相关配置来启用。
    (1)排除某些文件,如页面文件、资源文件等
    默认情况下,SpringBoot 的热部署功能对类路径的相关文件的修改都会生效。

拓展

对于 Java 类文件、应用配置文件 application.properties,以及通过 @PropertySource 注解引入的其他配置文件的修改会触发重新部署。

而对于类路径中资源文件的修改,包括 /META-INF/maven, /META-INF/resources,/resources,/static,/public 或者 /templates 等目录下面的相关页面模板、静态资源文件等都会触发重新加载,重新加载一般是指浏览器页面重新刷新。

  1. 如果我们希望这些文件的其中一部分的修改不会触发自动重启或者自动重新加载,则可以在 application.properties 文件内通过 spring.devtools.restart.exclude 属性来指定需要排除的文件,具体如下:静态目录 static 和公用目录 public 的文件的修改不要触发重新加载。
spring.devtools.restart.exclude=static/**,public/**

(2)监控额外的路径

有需要排除某些文件的需求,那么肯定有需要增加额外文件的监控的需求。SpringBoot 的热部署默认只会监控类路径的相关文件的修改,让类路径的文件的修改触发自动重新部署或者自动重新加载。但是如果我们希望其他路径的文件的修改也会触发热部署机制,是否可以做到?

答案是:可以的。与排除某些文件的配置类似,我们也可以在 application.properties 属性配置文件中通过 spring.devtools.restart.additional-paths 属性来指定需要额外监控的路径。

(3)禁用自动重启

当对 IDEA 开启自动编译以及在项目的 pom.xml 文件中添加 spring-boot-devtools 依赖之后,SpringBoot 的热部署功能是默认开启的。如果我们需要关闭热部署,禁用自动重启功能,如本地电脑很卡的时候(因为热部署会增加 CPU 的繁忙),则可以禁用掉自动重启功能。

具体为可以在 application.properties 文件配置 spring.devtools.restart.enabled=false,或者在启动命令行增加 -Dspring.devtools.restart.enabled=false 参数,或者在调用 SpringApplication.run() 之前调用 System.setProperty(“spring.devtools.restart.enabled”, “false”); 如下:

@SpringBootApplication
@PropertySource("classpath:/myconfig/custom.properties")
public class SpringBootDemoApplication {

    public static void main(String[] args) {
        System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }

}

3 SpringBoot 热部署的实现原理

以上介绍了 SpringBoot 的热部署功能的相关用法,同时基于 SpringBoot 热部署机制实现了应用的重新启动。相对于我们手动重新启动,热部署对应的重新启动速度会更快,所以也进一步提高了我们的开发效率。而这种速度的提升是与 SpringBoot 热部署的实现机制有关的。

3.1 类路径文件修改监视器

类路径中文件的修改会触发热部署机制,所以很容易想到需要一种机制来监视这些文件的变化,即需要一个类路径文件修改监视器来实时发现这些文件的修改。这个监视器在 SpringBoot 内部对应的实现是ClassPathFileSystemWatcher 类。

在创建 ClassPathFileSystemWatcher 对象时,会调用 afterPropertiesSet 方法来开启类路径文件的监视。具体实现如下:使用 FileSystemWatcher 定期监视文件是否发生变化,并且使用 ClassPathFileChangeListener 接收文件变化的事件,触发对应的事件处理器来完成应用的重启。

@Override
public void afterPropertiesSet() throws Exception {
   if (this.restartStrategy != null) {
      FileSystemWatcher watcherToStop = null;
      if (this.stopWatcherOnRestart) {
         watcherToStop = this.fileSystemWatcher;
      }
      // 添加类路径文件修改监听器
      this.fileSystemWatcher.addListener(
            new ClassPathFileChangeListener(this.applicationContext, this.restartStrategy, watcherToStop));
   }
   // 开始监视文件的修改
   this.fileSystemWatcher.start();
}

FileSystemWatcher 的 start 方法的实现如下:创建一个独立的线程 watchThread 来定期检查类路径的文件是否存在变化。其中检查的时间间隔由 pollInterval 和 quietPeriod 参数决定,默认为每秒检查一次。

public void start() {
   synchronized (this.monitor) {
      saveInitialSnapshots();
      if (this.watchThread == null) {
         Map<File, FolderSnapshot> localFolders = new HashMap<>();
         localFolders.putAll(this.folders);

         // 启用一个独立的线程来监视类路径的文件修改
         // 具体在Runnable 接口实现类 Watcher 定义
         this.watchThread = new Thread(new Watcher(this.remainingScans, new ArrayList<>(this.listeners),
               this.triggerFilter, this.pollInterval, this.quietPeriod, localFolders));
         this.watchThread.setName("File Watcher");
         this.watchThread.setDaemon(this.daemon);
         this.watchThread.start();
      }
   }
}
3.2 类加载器

SpringBoot 热部署实现的应用重启相对于我们手动重启,速度更快,其主要原因是 SpringBoot 使用了两个加载器来区分第三方功能 jar 包的类和项目自身定义的类。对于第三方 jar 包使用一个 base classloader 来加载,项目自身定义的类使用一个 restart classloader 来加载。

所以当类路径监视器发现类路径存放文件修改时,只需通知 restart classloader 类加载器重新加载项目自身定义的类即可,而对于第三方 jar 包的类则无需重新加载,所以加快了应用的启动速度。

4 总结

我们在这里总结一下本节的内容。在本节中我们主要介绍了 SpringBoot 热部署的概念和使用方法。通过热部署机制可以实现在开发过程中,代码和配置文件的修改自动生效,而无需手动重启应用;并且热部署的自动重启的速度更快,从而大大节省了我们在开发过程中用于等待应用重启的时间,提高了开发效率。

在实现层面,SpringBoot 的热部署机制主要是在内部通过一个文件监视线程 watcherThread 来监视类路径文件的变化,并且在发生变化时触发对应的事件来触发应用的自动重启。为了加快重启速度,SpringBoot 使用了两个类加载器来区分第三方 jar 包的类和项目自定义的类,在热部署时只需要重新加载项目自定义的类即可,从而加快了启动速度。