前言

在开发项目过程中,当修改了某些代码后需要本地验证时,需要重启本地服务进行验证,启动这个项目,如果项目庞大的话还是需要较长时间的,spring开发团队为我们带来了一个插件:spring-boot-devtools,很好的解决了本地验证缓慢的问题。

简单介绍

该原理其实很好说明,就是我们在编辑器上启动项目,然后改动相关的代码,然后编辑器自动触发编译替换掉历史的.class文件后,项目检测到有文件变更后会重启srpring-boot项目。

可以看看官网的触发描述:

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.

可以看到,我们引入了插件后,插件会监控我们classpath的资源变化,当classpath有变化后,会触发重启。很多文章会介绍如何配置自动触发,本人觉得不是很喜欢这种配置,当我们改动代码时,并不是改动一下就改动完的,我还是喜欢自己点击Build Project来触发重启。

The restart technology provided by Spring Boot works by using two classloaders. Classes that do not change (for example, those from third-party jars) are loaded into a base classloader. Classes that you are actively developing are loaded into a restart classloader. When the application is restarted, the restart classloader is thrown away and a new one is created. This approach means that application restarts are typically much faster than “cold starts”, since the base classloader is already available and populated.

这里提到了,该插件重启快速的原因:这里对类加载采用了两种类加载器,对于第三方jar包采用base-classloader来加载,对于开发人员自己开发的代码则使用restartClassLoader来进行加载,这使得比停掉服务重启要快的多,因为使用插件只是重启开发人员编写的代码部分。

我这边做个简单的验证:

@Component
@Slf4j
public class Devtools implements InitializingBean { @Override
public void afterPropertiesSet() {
log.info("guava-jar classLoader: " + BloomFilter.class.getClassLoader().toString());
log.info("Devtools ClassLoader: " + this.getClass().getClassLoader().toString());
} }

这边先去除spring-boot-devtools插件,跑下工程:

2020-01-21 22:26:27.182  INFO 16648 --- [           main] com.devtools.example.Devtools            : guava-jar classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
2020-01-21 22:26:27.182 INFO 16648 --- [ main] com.devtools.example.Devtools : Devtools ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2

可以看到,BloomFilter(第三方jar包)和Devtools(自己编写的类)使用的都是AppClassLoader加载的。

我们现在加上插件,然后执行下代码:

启动服务:
2020-01-22 10:05:37.575 INFO 20940 --- [ restartedMain] com.devtools.example.Devtools : guava-jar classLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
2020-01-22 10:05:37.575 INFO 20940 --- [ restartedMain] com.devtools.example.Devtools : Devtools ClassLoader: org.springframework.boot.devtools.restart.classloader.RestartClassLoader@3540628f 修改了代码插件自动重启:
2020-01-22 10:07:06.394 INFO 20940 --- [ restartedMain] com.devtools.example.Devtools : guava-jar classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
2020-01-22 10:07:06.394 INFO 20940 --- [ restartedMain] com.devtools.example.Devtools : Devtools ClassLoader: org.springframework.boot.devtools.restart.classloader.RestartClassLoader@769a8133

发现第三方的jar包的类加载器确实是使用的系统的类加载器,而我们自己写的代码的类加载器为RestartClassLoader,并且每次重启,类加载器的实例都会改变。

 
修改代码前.png
 
修改代码后.png

上图为代码修改前后类文件的变更。

代码解析

对于springboot的插件,都是从其插件中的spring.factories的配置文件开始的。

devtools-spring.factories.png

本地开发工具的配置类为:

org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration


/**
* Local Restart Configuration.
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration { @Autowired
private DevToolsProperties properties; @EventListener
public void 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() {
return new PatternClassPathRestartStrategy(
this.properties.getRestart().getAllExclude());
} @Bean
public HateoasObjenesisCacheDisabler hateoasObjenesisCacheDisabler() {
return new HateoasObjenesisCacheDisabler();
} @Bean
public FileSystemWatcherFactory fileSystemWatcherFactory() {
return new 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
public void onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(
new FileWatchingFailureHandler(fileSystemWatcherFactory()));
}
}

该类为监听到classpath的classpath的文件变更后,会触发ClassPathChangedEvent 事件,并会触发springboot的重启,其内部原理为使用了spring的事件监听机制,如果想补补这方面的内容可以看看我自己 写的这篇文章:Spring观察者模式原理解析

文件监听机制

下面看看其文件是如何被监听的

@Bean
@ConditionalOnMissingBean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
URL[] urls = Restarter.getInstance().getInitialUrls();
ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(
fileSystemWatcherFactory(), classPathRestartStrategy(), urls);
watcher.setStopWatcherOnRestart(true);
return watcher;
}

核心为该配置类,该类中包含了重启触发策略ClassPathRestartStrategy,以及监听的路径url和真正监听的实体类FileSystemWatcher。

public ClassPathFileSystemWatcher(FileSystemWatcherFactory fileSystemWatcherFactory,
ClassPathRestartStrategy restartStrategy, URL[] urls) {
Assert.notNull(fileSystemWatcherFactory,
"FileSystemWatcherFactory must not be null");
Assert.notNull(urls, "Urls must not be null");
this.fileSystemWatcher = fileSystemWatcherFactory.getFileSystemWatcher();
this.restartStrategy = restartStrategy;
this.fileSystemWatcher.addSourceFolders(new ClassPathFolders(urls));
}

打断点进去发现:

调试.png

其传入的urls即为IDE编译代码的路径,已经其触发重启策略中已剔除掉配置项和一些测试的二进制文件。

该类ClassPathFileSystemWatcher实例化之后会调用其afterPropertiesSet方法(实现了InitializingBean)

@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();
}

可以看到其加入了个ClassPathFileChangeListener对象,后续该对象是触发ClassPathChangedEvent事件的实现者。

ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher,
ClassPathRestartStrategy restartStrategy,
FileSystemWatcher fileSystemWatcherToStop) {
Assert.notNull(eventPublisher, "EventPublisher must not be null");
Assert.notNull(restartStrategy, "RestartStrategy must not be null");
this.eventPublisher = eventPublisher;
this.restartStrategy = restartStrategy;
this.fileSystemWatcherToStop = fileSystemWatcherToStop;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
boolean restart = isRestartRequired(changeSet);
publishEvent(new ClassPathChangedEvent(this, changeSet, restart));
} private void publishEvent(ClassPathChangedEvent event) {
this.eventPublisher.publishEvent(event);
if (event.isRestartRequired() && this.fileSystemWatcherToStop != null) {
this.fileSystemWatcherToStop.stop();
}
}

接着上述代码分析,this.fileSystemWatcher.start(),该代码为监听文件变化的核心,看看其源码

/**
* Start monitoring the source folder for changes.
*/
public void start() {
synchronized (this.monitor) {
saveInitialSnapshots();
if (this.watchThread == null) {
Map<File, FolderSnapshot> localFolders = new HashMap<File, FolderSnapshot>();
localFolders.putAll(this.folders);
this.watchThread = new Thread(new Watcher(this.remainingScans,
new ArrayList<FileChangeListener>(this.listeners),
this.triggerFilter, this.pollInterval, this.quietPeriod,
localFolders));
this.watchThread.setName("File Watcher");
this.watchThread.setDaemon(this.daemon);
this.watchThread.start();
}
}
}

首先,先保存urls路径下文件及文件夹的快照信息,包括文件的长度以及其最后修改时间,该信息以FolderSnapshot、FileSnapshot类中进行保存。文件的快照信息在该属性中保存:fileSystemWatcher中的private final Map<File, FolderSnapshot> folders = new HashMap<File, FolderSnapshot>();

往下分析,创建了一个File Watcher的线程,将文件快照信息和listeners(触发文件变更事件)当做属性以Watcher对象(实现了Runnable接口)传入线程中,并启动线程。

private static final class 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
public void 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();
}
}; private void 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());
}
} private boolean isDifferent(Map<File, FolderSnapshot> previous,
Map<File, FolderSnapshot> current) {
if (!previous.keySet().equals(current.keySet())) {
return true;
}
for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {
FolderSnapshot previousFolder = entry.getValue();
FolderSnapshot currentFolder = current.get(entry.getKey());
if (!previousFolder.equals(currentFolder, this.triggerFilter)) {
return true;
}
}
return false;
} 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;
} private void 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;
} private void fireListeners(Set<ChangedFiles> changeSet) {
for (FileChangeListener listener : this.listeners) {
listener.onChange(changeSet);
}
} }

可以看到,线程在scan中不断的做文件的扫描判断,看看当前的文件快照和前一个文件的快照是否有变化(毫秒级),若有变化则会执行updateSnapshots方法,并触发listener.onChange(changeSet)方法,发布ClassPathChangedEvent事件,引发重启。

File-Watcher
"File Watcher" #51 daemon prio=5 os_prio=0 tid=0x0000000017276000 nid=0x3c04 waiting on condition [0x000000001a66f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.scan(FileSystemWatcher.java:250)
at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.run(FileSystemWatcher.java:240)
at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers:
- None

可以看到,后台确实是启动了一个线程不断的在做文件快照的检查工作。

这里,文件检测并触发的逻辑已经介绍完毕,后续接着介绍服务重启的详细流程。

spring-boot-devtools热部署揭秘的更多相关文章

  1. spring boot devtools热部署

    问题1: Springloaded 在springboot2的maven的pom.xml 无法找到 解决方法:在idea通过View->Tool Windows->Maven Projec ...

  2. Spring boot之热部署

    springboot热部署 1.springloaded(热部署) 2.devtools(热部署) 一.springloaded 问题的提出: 在编写代码的时候,你会发现我们只是简单把打印信息改变了, ...

  3. Spring Boot (3) 热部署devtools

    热部署:当发现程序修改时自动启动应用程序. spring boot为开发者提供了一个名为spring-boot-devtools的模块来使sring boot应用支持热部署,提高开发者的开发效率,无需 ...

  4. spring boot 之热部署(三)

    热部署:当发现程序修改时自动启动应用程序. spring boot使用的是spring-boot-devtools是一个为开发者服务的一个模块.其原理用了classLoader 其中一个加载不变的类, ...

  5. Spring Boot实现热部署

    在Spring Boot实现代码热部署是一件很简单的事情,代码的修改可以自动部署并重新热启动项目. 引用devtools依赖 <dependency> <groupId>org ...

  6. spring boot 之热部署

    热部署:当发现程序修改时自动启动应用程序. spring boot使用的是spring-boot-devtools是一个为开发者服务的一个模块.其原理用了classLoader 其中一个加载不变的类, ...

  7. Spring boot的热部署

    当把配置文件,比如yml也打到jar包后,如何修改配置,而又不用重新发布呢? 在jar包同一目录下,放置Application.yml (注意,不管jar包内是否此文件名)修改配置文件后,重新启动ja ...

  8. IDEA下配置Spring Boot的热部署

    © 版权声明:本文为博主原创文章,转载请注明出处 devtools简介 spring-boot-devtools会监听classpath下的文件变动,并且会立即重启应用(发生在保存时机),因为其采用的 ...

  9. spring boot 调试 - 热部署

    maven gradle Maven: 命令行方式: mvn spring-boot:run -Drun.jvmArguments="-Xdebug -Xrunjdwp:transport= ...

  10. 1. Spring boot 之热部署

    1. spring boot 热部署 1.1. springloaded springloaded可以实现修改类文件的热部署.下载地址:springloaded 安装单击Run Configurati ...

随机推荐

  1. 【stm32】的PWM外设|学习笔记

    PWM简单理解就是生成PWM方波,主要可控的参数有周期,占空比,极性. stm32内置PWM生成器,我想应该是用TIM加比较器来实现的吧,那么如何实现stm32输出PWM波呢? 一.得知道那些IO可以 ...

  2. 像MIUI一样做Zabbix二次开发(6)——应用场景和规划

    其他使用场景 监控做为一个重要的管理手段,存在很多的使用场景,简单列举我们现在碰到的: 1.     系统集成 事件管理流程集成:配置管理集成,自动CI获取,提高CMDB准确.实时性:知识库集成,提高 ...

  3. 在vite中怎么批量注册组件

    1. 在webpack中使用require来获取组件 / 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配 //匹配当前文件夹下的所有.vue文件 注册全局组件 const importF ...

  4. Git中常见的一些问题总结

    Git中常见的一些问题总结 目录 Git中常见的一些问题总结 1.git添加到暂存区的修改,如果不想要了,怎么撤销 2.git添加到本地仓库,如果不想要了,怎么撤销 3.提交代码时发现别人已经提交了( ...

  5. drf从入门到飞升仙界 03

    APIView执行流程 基于APIView+JsonResponse编写接口 # APIView是drf提供给使用者的一个类,在使用drf写视图类,继承都是这个类及其子类 # APIView继承了Dj ...

  6. Spring Security 自定义认证逻辑

    Spring Security 自定义认证逻辑 这篇文章的内容基于对Spring Security 认证流程的理解,如果你不了解,可以读一下这篇文章:Spring Security 认证流程 . 分析 ...

  7. PHP 阿里云短信验证码的实现

    Test.php是测试你的环境是否部署完成 demo里的sendSms.php里修改 // fixme 必填: 请参阅 https://ak-console.aliyun.com/ 取得您的AK信息$ ...

  8. 实现一个网页同时调用多个倒计时 jquery/js

    最近需要网页添加多个倒计时. 查阅网络,基本上都是千遍一律的不好用. 自己按需写了个.希望对大家有用. 有用请赞一个哦! //js2 var plugJs={ stamp:0, tid:1, stam ...

  9. Carthage 使用介绍

    1.安装 Carthage 安装 brew install carthage 检测当前版本 carthage version 升级至最新版本 brew upgrade carthage 2.如果更新出 ...

  10. centos7部署elasticsearch-7.16.2分布式集群(此版本修复了log4j2漏洞)

    简介 Elasticsearch 是一个分布式.RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例. 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之 ...