1 如何开启
1.1 pom配置
<!--SpringBoot热部署配置 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></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
@Configuration(proxyBeanMethods = false)@ConditionalOnInitializedRestarter@EnableConfigurationProperties({DevToolsProperties.class})public class LocalDevToolsAutoConfiguration {//监听到了ClassPathChangedEvent事件,就stop,重新加载最新的class文件再restart@BeanApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(FileSystemWatcherFactory fileSystemWatcherFactory) {return (event) -> {if (event.isRestartRequired()) {Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));}};}@BeanFileSystemWatcherFactory fileSystemWatcherFactory() {return this::newFileSystemWatcher;}private FileSystemWatcher newFileSystemWatcher() {Restart restartProperties = this.properties.getRestart();//文件监听器FileSystemWatcher watcher = new FileSystemWatcher(true, restartProperties.getPollInterval(), restartProperties.getQuietPeriod());//省略return watcher;}//FileSystemWatcher 里面有一个关键方法如下,addListener会由spring ioc过程的afterPropertiesSet方法调用,会添加一个ClassPathFileChangeListenerpublic void addListener(FileChangeListener fileChangeListener) {this.listeners.add(fileChangeListener);}//ClassPathFileChangeListener里面有一个关键方法如下public void onChange(Set<ChangedFiles> changeSet) {boolean restart = this.isRestartRequired(changeSet);this.publishEvent(new ClassPathChangedEvent(this, changeSet, restart));}}
参考:https://www.jianshu.com/p/9e5c0d80ec74
2.2 SpringBoot类加载器
SpringBoot项目将系统的类拆分到两个ClassLoader。
- 静态ClassLoader 系统默认的
- 动态ClassLoader restart.classloader
使用静态ClassLoader和动态ClassLoader隔离的加载方式。热部署的时候,静态类加载器保持不变,仅仅让动态类加载器加载所需要的类
本地开发工具的配置类为: org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration
/*** Local Restart Configuration.*/@Configuration@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)staticclass RestartConfiguration {@Autowiredprivate DevToolsProperties properties;@EventListenerpublicvoid onClassPathChanged(ClassPathChangedEvent event) {if (event.isRestartRequired()) {Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory()));}}@Bean@ConditionalOnMissingBeanpublic ClassPathFileSystemWatcher classPathFileSystemWatcher() {URL[] urls = Restarter.getInstance().getInitialUrls();ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(fileSystemWatcherFactory(), classPathRestartStrategy(), urls);watcher.setStopWatcherOnRestart(true);return watcher;}@Bean@ConditionalOnMissingBeanpublic ClassPathRestartStrategy classPathRestartStrategy() {returnnew PatternClassPathRestartStrategy(this.properties.getRestart().getAllExclude());}@Beanpublic HateoasObjenesisCacheDisabler hateoasObjenesisCacheDisabler() {returnnew HateoasObjenesisCacheDisabler();}@Beanpublic FileSystemWatcherFactory fileSystemWatcherFactory() {returnnew FileSystemWatcherFactory() {@Overridepublic FileSystemWatcher getFileSystemWatcher() {return newFileSystemWatcher();}};}private FileSystemWatcher newFileSystemWatcher() {Restart restartProperties = this.properties.getRestart();FileSystemWatcher watcher = new FileSystemWatcher(true,restartProperties.getPollInterval(),restartProperties.getQuietPeriod());String triggerFile = restartProperties.getTriggerFile();if (StringUtils.hasLength(triggerFile)) {watcher.setTriggerFilter(new TriggerFileFilter(triggerFile));}List<File> additionalPaths = restartProperties.getAdditionalPaths();for (File path : additionalPaths) {watcher.addSourceFolder(path.getAbsoluteFile());}return watcher;}}@EventListenerpublicvoid onClassPathChanged(ClassPathChangedEvent event) {if (event.isRestartRequired()) {Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory()));}}
该类为监听到classpath的classpath的文件变更后,会触发ClassPathChangedEvent 事件,并会触发springboot的重启类加载
3 文件监听机制
先保存urls路径下文件及文件夹的快照信息,包括文件的长度以及其最后修改时间,该信息以FolderSnapshot、FileSnapshot类中进行保存。文件的快照信息在该属性中保存:fileSystemWatcher中的private final Map
往下分析,创建了一个File Watcher的线程,将文件快照信息和listeners(触发文件变更事件)当做属性以Watcher对象(实现了Runnable接口)传入线程中,并启动线程。
privatestaticfinalclass Watcher implements Runnable {private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners,FileFilter triggerFilter, long pollInterval, long quietPeriod,Map<File, FolderSnapshot> folders) {this.remainingScans = remainingScans;this.listeners = listeners;this.triggerFilter = triggerFilter;this.pollInterval = pollInterval;this.quietPeriod = quietPeriod;this.folders = folders;}@Overridepublicvoid run() {int remainingScans = this.remainingScans.get();//-1(AtomicInteger)while (remainingScans > 0 || remainingScans == -1) {try {if (remainingScans > 0) {this.remainingScans.decrementAndGet();}scan();}catch (InterruptedException ex) {Thread.currentThread().interrupt();}remainingScans = this.remainingScans.get();}};privatevoid scan() throws InterruptedException {Thread.sleep(this.pollInterval - this.quietPeriod);Map<File, FolderSnapshot> previous;Map<File, FolderSnapshot> current = this.folders;do {previous = current;current = getCurrentSnapshots();Thread.sleep(this.quietPeriod);}while (isDifferent(previous, current));if (isDifferent(this.folders, current)) {updateSnapshots(current.values());}}privateboolean isDifferent(Map<File, FolderSnapshot> previous,Map<File, FolderSnapshot> current) {if (!previous.keySet().equals(current.keySet())) {returntrue;}for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {FolderSnapshot previousFolder = entry.getValue();FolderSnapshot currentFolder = current.get(entry.getKey());if (!previousFolder.equals(currentFolder, this.triggerFilter)) {returntrue;}}returnfalse;}private Map<File, FolderSnapshot> getCurrentSnapshots() {Map<File, FolderSnapshot> snapshots = new LinkedHashMap<File, FolderSnapshot>();for (File folder : this.folders.keySet()) {snapshots.put(folder, new FolderSnapshot(folder));}return snapshots;}privatevoid updateSnapshots(Collection<FolderSnapshot> snapshots) {Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();for (FolderSnapshot snapshot : snapshots) {FolderSnapshot previous = this.folders.get(snapshot.getFolder());updated.put(snapshot.getFolder(), snapshot);ChangedFiles changedFiles = previous.getChangedFiles(snapshot,this.triggerFilter);if (!changedFiles.getFiles().isEmpty()) {changeSet.add(changedFiles);}}if (!changeSet.isEmpty()) {fireListeners(Collections.unmodifiableSet(changeSet));}this.folders = updated;}privatevoid fireListeners(Set<ChangedFiles> changeSet) {for (FileChangeListener listener : this.listeners) {listener.onChange(changeSet);}}}
4 重新加载

重启的过程中呢,包括两个步骤,第一步 stop ,
**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);
}
**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 /> }


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

