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
@Bean
ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(FileSystemWatcherFactory fileSystemWatcherFactory) {
return (event) -> {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));
}
};
}
@Bean
FileSystemWatcherFactory 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方法调用,会添加一个ClassPathFileChangeListener
public 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 {
@Autowired
private DevToolsProperties properties;
@EventListener
publicvoid onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(
new FileWatchingFailureHandler(fileSystemWatcherFactory()));
}
}
@Bean
@ConditionalOnMissingBean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
URL[] urls = Restarter.getInstance().getInitialUrls();
ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(
fileSystemWatcherFactory(), classPathRestartStrategy(), urls);
watcher.setStopWatcherOnRestart(true);
return watcher;
}
@Bean
@ConditionalOnMissingBean
public ClassPathRestartStrategy classPathRestartStrategy() {
returnnew PatternClassPathRestartStrategy(
this.properties.getRestart().getAllExclude());
}
@Bean
public HateoasObjenesisCacheDisabler hateoasObjenesisCacheDisabler() {
returnnew HateoasObjenesisCacheDisabler();
}
@Bean
public FileSystemWatcherFactory fileSystemWatcherFactory() {
returnnew FileSystemWatcherFactory() {
@Override
public 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;
}
}
@EventListener
publicvoid 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;
}
@Override
publicvoid 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 方法,完成重启