1 如何开启

1.1 pom配置

  1. <!--SpringBoot热部署配置 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-devtools</artifactId>
  5. <scope>runtime</scope>
  6. <optional>true</optional>
  7. </dependency>

1.2 设置application.properties
spring.devtools.restart.enabled=true

2 原理

2.1 监听文件变化

找到 spring.factories 文件172个部署
spring ioc过程注册一个ClassPathFileChangeListener专门监听文件改动事件,当发现文件改动时发布事件ClassPathChangedEvent,当LocalDevToolsAutoConfiguration 监听到了ClassPathChangedEvent事件时就知道文件发生改动就会stop应用,gc,清除对象引用,再重新加载新文件,restart

  1. @Configuration(
  2. proxyBeanMethods = false
  3. )
  4. @ConditionalOnInitializedRestarter
  5. @EnableConfigurationProperties({DevToolsProperties.class})
  6. public class LocalDevToolsAutoConfiguration {
  7. //监听到了ClassPathChangedEvent事件,就stop,重新加载最新的class文件再restart
  8. @Bean
  9. ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(FileSystemWatcherFactory fileSystemWatcherFactory) {
  10. return (event) -> {
  11. if (event.isRestartRequired()) {
  12. Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));
  13. }
  14. };
  15. }
  16. @Bean
  17. FileSystemWatcherFactory fileSystemWatcherFactory() {
  18. return this::newFileSystemWatcher;
  19. }
  20. private FileSystemWatcher newFileSystemWatcher() {
  21. Restart restartProperties = this.properties.getRestart();
  22. //文件监听器
  23. FileSystemWatcher watcher = new FileSystemWatcher(true, restartProperties.getPollInterval(), restartProperties.getQuietPeriod());
  24. //省略
  25. return watcher;
  26. }
  27. //FileSystemWatcher 里面有一个关键方法如下,addListener会由spring ioc过程的afterPropertiesSet方法调用,会添加一个ClassPathFileChangeListener
  28. public void addListener(FileChangeListener fileChangeListener) {
  29. this.listeners.add(fileChangeListener);
  30. }
  31. //ClassPathFileChangeListener里面有一个关键方法如下
  32. public void onChange(Set<ChangedFiles> changeSet) {
  33. boolean restart = this.isRestartRequired(changeSet);
  34. this.publishEvent(new ClassPathChangedEvent(this, changeSet, restart));
  35. }
  36. }

参考:https://www.jianshu.com/p/9e5c0d80ec74

2.2 SpringBoot类加载器

SpringBoot项目将系统的类拆分到两个ClassLoader。

  • 静态ClassLoader 系统默认的
  • 动态ClassLoader restart.classloader
    使用静态ClassLoader和动态ClassLoader隔离的加载方式。热部署的时候,静态类加载器保持不变,仅仅让动态类加载器加载所需要的类

本地开发工具的配置类为: org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration

  1. /**
  2. * Local Restart Configuration.
  3. */
  4. @Configuration
  5. @ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
  6. staticclass RestartConfiguration {
  7. @Autowired
  8. private DevToolsProperties properties;
  9. @EventListener
  10. publicvoid onClassPathChanged(ClassPathChangedEvent event) {
  11. if (event.isRestartRequired()) {
  12. Restarter.getInstance().restart(
  13. new FileWatchingFailureHandler(fileSystemWatcherFactory()));
  14. }
  15. }
  16. @Bean
  17. @ConditionalOnMissingBean
  18. public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
  19. URL[] urls = Restarter.getInstance().getInitialUrls();
  20. ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(
  21. fileSystemWatcherFactory(), classPathRestartStrategy(), urls);
  22. watcher.setStopWatcherOnRestart(true);
  23. return watcher;
  24. }
  25. @Bean
  26. @ConditionalOnMissingBean
  27. public ClassPathRestartStrategy classPathRestartStrategy() {
  28. returnnew PatternClassPathRestartStrategy(
  29. this.properties.getRestart().getAllExclude());
  30. }
  31. @Bean
  32. public HateoasObjenesisCacheDisabler hateoasObjenesisCacheDisabler() {
  33. returnnew HateoasObjenesisCacheDisabler();
  34. }
  35. @Bean
  36. public FileSystemWatcherFactory fileSystemWatcherFactory() {
  37. returnnew FileSystemWatcherFactory() {
  38. @Override
  39. public FileSystemWatcher getFileSystemWatcher() {
  40. return newFileSystemWatcher();
  41. }
  42. };
  43. }
  44. private FileSystemWatcher newFileSystemWatcher() {
  45. Restart restartProperties = this.properties.getRestart();
  46. FileSystemWatcher watcher = new FileSystemWatcher(true,
  47. restartProperties.getPollInterval(),
  48. restartProperties.getQuietPeriod());
  49. String triggerFile = restartProperties.getTriggerFile();
  50. if (StringUtils.hasLength(triggerFile)) {
  51. watcher.setTriggerFilter(new TriggerFileFilter(triggerFile));
  52. }
  53. List<File> additionalPaths = restartProperties.getAdditionalPaths();
  54. for (File path : additionalPaths) {
  55. watcher.addSourceFolder(path.getAbsoluteFile());
  56. }
  57. return watcher;
  58. }
  59. }
  60. @EventListener
  61. publicvoid onClassPathChanged(ClassPathChangedEvent event) {
  62. if (event.isRestartRequired()) {
  63. Restarter.getInstance().restart(
  64. new FileWatchingFailureHandler(fileSystemWatcherFactory()));
  65. }
  66. }

该类为监听到classpath的classpath的文件变更后,会触发ClassPathChangedEvent 事件,并会触发springboot的重启类加载

3 文件监听机制

先保存urls路径下文件及文件夹的快照信息,包括文件的长度以及其最后修改时间,该信息以FolderSnapshot、FileSnapshot类中进行保存。文件的快照信息在该属性中保存:fileSystemWatcher中的private final Map folders = new HashMap();
往下分析,创建了一个File Watcher的线程,将文件快照信息和listeners(触发文件变更事件)当做属性以Watcher对象(实现了Runnable接口)传入线程中,并启动线程。

  1. privatestaticfinalclass Watcher implements Runnable {
  2. private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners,
  3. FileFilter triggerFilter, long pollInterval, long quietPeriod,
  4. Map<File, FolderSnapshot> folders) {
  5. this.remainingScans = remainingScans;
  6. this.listeners = listeners;
  7. this.triggerFilter = triggerFilter;
  8. this.pollInterval = pollInterval;
  9. this.quietPeriod = quietPeriod;
  10. this.folders = folders;
  11. }
  12. @Override
  13. publicvoid run() {
  14. int remainingScans = this.remainingScans.get();//-1(AtomicInteger)
  15. while (remainingScans > 0 || remainingScans == -1) {
  16. try {
  17. if (remainingScans > 0) {
  18. this.remainingScans.decrementAndGet();
  19. }
  20. scan();
  21. }
  22. catch (InterruptedException ex) {
  23. Thread.currentThread().interrupt();
  24. }
  25. remainingScans = this.remainingScans.get();
  26. }
  27. };
  28. privatevoid scan() throws InterruptedException {
  29. Thread.sleep(this.pollInterval - this.quietPeriod);
  30. Map<File, FolderSnapshot> previous;
  31. Map<File, FolderSnapshot> current = this.folders;
  32. do {
  33. previous = current;
  34. current = getCurrentSnapshots();
  35. Thread.sleep(this.quietPeriod);
  36. }
  37. while (isDifferent(previous, current));
  38. if (isDifferent(this.folders, current)) {
  39. updateSnapshots(current.values());
  40. }
  41. }
  42. privateboolean isDifferent(Map<File, FolderSnapshot> previous,
  43. Map<File, FolderSnapshot> current) {
  44. if (!previous.keySet().equals(current.keySet())) {
  45. returntrue;
  46. }
  47. for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {
  48. FolderSnapshot previousFolder = entry.getValue();
  49. FolderSnapshot currentFolder = current.get(entry.getKey());
  50. if (!previousFolder.equals(currentFolder, this.triggerFilter)) {
  51. returntrue;
  52. }
  53. }
  54. returnfalse;
  55. }
  56. private Map<File, FolderSnapshot> getCurrentSnapshots() {
  57. Map<File, FolderSnapshot> snapshots = new LinkedHashMap<File, FolderSnapshot>();
  58. for (File folder : this.folders.keySet()) {
  59. snapshots.put(folder, new FolderSnapshot(folder));
  60. }
  61. return snapshots;
  62. }
  63. privatevoid updateSnapshots(Collection<FolderSnapshot> snapshots) {
  64. Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();
  65. Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
  66. for (FolderSnapshot snapshot : snapshots) {
  67. FolderSnapshot previous = this.folders.get(snapshot.getFolder());
  68. updated.put(snapshot.getFolder(), snapshot);
  69. ChangedFiles changedFiles = previous.getChangedFiles(snapshot,
  70. this.triggerFilter);
  71. if (!changedFiles.getFiles().isEmpty()) {
  72. changeSet.add(changedFiles);
  73. }
  74. }
  75. if (!changeSet.isEmpty()) {
  76. fireListeners(Collections.unmodifiableSet(changeSet));
  77. }
  78. this.folders = updated;
  79. }
  80. privatevoid fireListeners(Set<ChangedFiles> changeSet) {
  81. for (FileChangeListener listener : this.listeners) {
  82. listener.onChange(changeSet);
  83. }
  84. }
  85. }

4 重新加载

image.png

重启的过程中呢,包括两个步骤,第一步 stop

  1. **protectedvoid** stop() **throws** Exception {<br /> **this.**logger**.**debug(**"Stopping application"**);<br /> **this.**stopLock**.**lock();<br /> **try** {<br /> **for** (ConfigurableApplicationContext context : **this.**rootContexts) {<br /> context**.**close();<br /> **this.**rootContexts**.**remove(context);<br /> }<br /> cleanupCaches();<br /> **if** (**this.**forceReferenceCleanup) {<br /> forceReferenceCleanup();<br /> }<br /> }<br /> **finally** {<br /> **this.**stopLock**.**unlock();<br /> }<br /> System**.**gc();<br /> System**.**runFinalization();<br /> }

第二步 start
protectedvoid start(FailureHandler failureHandler) throws Exception {
do {
Throwable error = doStart();
if (error == null) {
return;
}
if (failureHandler.handle(error) == Outcome.ABORT) {
return;
}
}
while (true);
}

  1. **private** Throwable doStart() **throws** Exception {<br /> Assert**.**notNull(**this.**mainClassName, **"Unable to find the main class to restart"**);<br /> URL[] urls = **this.**urls**.**toArray(**new** URL[**0**]);<br /> ClassLoaderFiles updatedFiles = **new** ClassLoaderFiles(**this.**classLoaderFiles);<br /> ClassLoader classLoader = **new** RestartClassLoader(**this.**applicationClassLoader, urls, updatedFiles);<br /> **if** (**this.**logger**.**isDebugEnabled()) {<br /> **this.**logger**.**debug(**"Starting application "** + **this.**mainClassName + **" with URLs "** + Arrays**.**asList(urls));<br /> }<br /> **return** relaunch(classLoader);<br /> }

image.png

image.png

start 的过程中,是通过创建这个重启线程 RestartLauncher 来实现的,可以发现该类的任务就是找到 mainclass 并调用 main 方法,完成重启

image.png

Spring Loaded

参考: 1https://www.shouxicto.com/article/1867.html 最全