前言
最近在阅读 spring cloud 源码的时候 发现 spring devtools 这个包 觉得比较有趣, 就研究了一下. 然后写了这篇文章.
主要解决三个疑问 1 如何初始化 2 如何实时监听 3 如何远程重启
1 构造
Restarter
Restarter 是在 spring 容器启动过程中通过 RestartApplicationListener 接受 ApplicationStartingEvent 广播然后进行一系列初始化操作并实时监听 首先 RestartApplicationListener 接受 ApplicationStartingEvent 事件广播并判断 spring.devtools.restart.enabled 是否开启如果开启就进行初始化如下操作
- private void onApplicationStartingEvent(ApplicationStartingEvent event) {
- String enabled = System.getProperty("spring.devtools.restart.enabled");
- if (enabled != null && !Boolean.parseBoolean(enabled)) {
- Restarter.disable();
- } else {
- String[] args = event.getArgs();
- DefaultRestartInitializer initializer = new DefaultRestartInitializer();
- boolean restartOnInitialize = !AgentReloader.isActive();
- Restarter.initialize(args, false, initializer, restartOnInitialize);
- }
- }
然后调用如下初始化方法
- protected void initialize(boolean restartOnInitialize) {
- this.preInitializeLeakyClasses();
- if (this.initialUrls != null) {
- this.urls.addAll(Arrays.asList(this.initialUrls));
- if (restartOnInitialize) {
- this.logger.debug("Immediately restarting application");
- this.immediateRestart();
- }
- }
- }
- private void immediateRestart() {
- try {
- this.getLeakSafeThread().callAndWait(() -> {
- this.start(FailureHandler.NONE);
- this.cleanupCaches();
- return null;
- });
- } catch (Exception var2) {
- this.logger.warn("Unable to initialize restarter", var2);
- }
- SilentExitExceptionHandler.exitCurrentThread();
- }
由上面代码可知在 immediateRestart 方法中会再开一个线程执行 this.start(FailureHandler.NONE)方法, 这个方法会新起一个线程去初始化上下文, 当项目结束后再返回, 如下代码
- protected void start(FailureHandler failureHandler) throws Exception {
- Throwable error;
- do {
- error = this.doStart();
- if (error == null) {
- return;
- }
- } while(failureHandler.handle(error) != Outcome.ABORT);
- }
- private Throwable doStart() throws Exception {
- Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
- URL[] urls = (URL[])this.urls.toArray(new URL[0]);
- ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
- ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger);
- if (this.logger.isDebugEnabled()) {
- this.logger.debug("Starting application" + this.mainClassName + "with URLs" + Arrays.asList(urls));
- }
- return this.relaunch(classLoader);
- }
- protected Throwable relaunch(ClassLoader classLoader) throws Exception {
- RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args, this.exceptionHandler);
- launcher.start();
- launcher.join();
- return launcher.getError();
- }
由上面代码可知, Restarter 会启动 RestartLauncher 线程然后启动后就将当前线程挂起, 等待 RestartLauncher 线程任务完成. 再来看看 RestartLauncher 线程执行的任务
- public void run() {
- try {
- Class<?> mainClass = this.getContextClassLoader().loadClass(this.mainClassName);
- Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
- mainMethod.invoke((Object)null, this.args);
- } catch (Throwable var3) {
- this.error = var3;
- this.getUncaughtExceptionHandler().uncaughtException(this, var3);
- }
- }
由上面代码可知, RestartLauncher 线程会执行启动类的 main 方法相当于重新创建应用上下文
总结
由上面的流程可知当第一次执行的时候, 如果没有关闭 spring developer 那么就会创建 Restarter 并将当前线程挂起然后重新起一个新的子线程来创建应用上下文
2 实时监听
主要是通过类 FileSystemWatcher 进行实时监听 首先启动过程如下 1 在构建 Application 上下文的时候 refreshContext 创建 bean 的时候会扫描 LocalDevToolsAutoConfiguration 配置的 ClassPathFileSystemWatcher 进行初始化 并同时初始化对应依赖 如下图
- @Bean
- @ConditionalOnMissingBean
- public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
- URL[] urls = Restarter.getInstance().getInitialUrls();
- ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(
- fileSystemWatcherFactory(), classPathRestartStrategy(), urls);
- watcher.setStopWatcherOnRestart(true);
- return watcher;
- }
- @Bean
- public FileSystemWatcherFactory fileSystemWatcherFactory() {
- return this::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;
- }
2 然后会调用 ClassPathFileSystemWatcher 中 InitializingBean 接口所对应的 afterPropertiesSet 方法去启动一个 fileSystemWatcher , 在启动 fileSystemWatcher 的时候会在 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();
- }
3 fileSystemWatcher 内部会启动一个 Watcher 线程用于循环监听目录变动, 如果发生变动就会发布一个 onChange 通知到所有注册的 FileChangeListener 上去 如下代码
- public void start() {
- synchronized (this.monitor) {
- saveInitialSnapshots();
- if (this.watchThread == null) {
- Map<File, FolderSnapshot> localFolders = new HashMap<>();
- localFolders.putAll(this.folders);
- 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();
- }
- }
- }
------------------------------------Watcher 中的内部执行方法 -----------------------------------------------------------------------@Override
- public void run() {
- int remainingScans = this.remainingScans.get();
- while (remainingScans> 0 || remainingScans == -1) {
- try {
- if (remainingScans> 0) {
- this.remainingScans.decrementAndGet();
- }
- scan(); // 监听变动并发布通知
- }
- catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- remainingScans = this.remainingScans.get();
- }
- }
4 之前注册的 ClassPathFileChangeListener 监听器收到通知后会发布一个 ClassPathChangedEvent(ApplicationEvent)事件, 如果需要重启就中断当前监听线程. 如下代码
- @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();
- }
- }
5 上边发布的 ClassPathChangedEvent 事件会被 LocalDevToolsAutoConfiguration 中配置的监听器监听到然后如果需要重启就调用 Restarter 的方法进行重启 如下
- @EventListener
- public void onClassPathChanged(ClassPathChangedEvent event) {
- if (event.isRestartRequired()) {
- Restarter.getInstance().restart(
- new FileWatchingFailureHandler(fileSystemWatcherFactory()));
- }
- }
3 LiveReload
liveReload 用于在修改了源码并重启之后刷新浏览器 可通过 spring.devtools.livereload.enabled = false 关闭
4 远程重启
在查看 devtools 源码的时候还有一个包 (org.springframework.boot.devtools.remote) 感觉挺有意思的, 通过查资料得知, 这个包可以用于远程提交代码并重启, 所以研究了一下 因为对这里的实际操作不太感兴趣所有以下摘抄自
Spring Boot 的开发者工具不仅仅局限于本地开发. 你也可以应用在远程应用上. 远程应用是可选的. 如果你想开启, 你需要把 devtools 的包加到你的打包的 jar 中:
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <excludeDevtools>false</excludeDevtools>
- </configuration>
- </plugin>
- </plugins>
- </build>
然后, 你还需要设置一个远程访问的秘钥 spring.devtools.remote.secret:
spring.devtools.remote.secret=mysecret
开启远程开发功能是有风险的. 永远不要在一个真正的生产机器上这么用.
远程应用支持两个方面的功能; 一个是服务端, 一个是客户端. 只要你设置了 spring.devtools.remote.secret, 服务端就会自动开启. 客户端需要你手动来开启.
运行远程应用的客户端
远程应用的客户端被设计成在你的 IDE 中运行. 你需要在拥有和你的远程应用相同的 classpath 的前提下, 运行 org.springframework.boot.devtools.RemoteSpringApplication. 这个 application 的参数就是你要连接的远程应用的 URL.
例如, 如果你用的是 Eclipse 或者 STS, 你有一个项目叫 my-App, 你已经部署在云平台上了, 你需要这么做:
从 Run 菜单选择 Run Configurations...
创建一个 Java Application 的启动配置
使用 org.springframework.boot.devtools.RemoteSpringApplication 作为启动类
把 https://myapp.cfapps.io 作为程序的参数(这个 URL 是你真正的 URL)
一个启动的远程应用是这样的:
- . ____ _ __ _ _
- /\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \
- ( ( )\___ | '_ |'_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \
- \\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) )
- ' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ // /
- =========|_|==============|___/===================================/_/_/_/
- :: Spring Boot Remote :: 1.5.3.RELEASE
- 2015-06-10 18:25:06.632 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Starting RemoteSpringApplication on pwmbp with PID 14938 (/Users/pwebb/projects/spring-boot/code/spring-boot-devtools/target/classes started by pwebb in /Users/pwebb/projects/spring-boot/code/spring-boot-samples/spring-boot-sample-devtools)
- 2015-06-10 18:25:06.671 INFO 14938 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2a17b7b6: startup date [Wed Jun 10 18:25:06 PDT 2015]; root of context hierarchy
- 2015-06-10 18:25:07.043 WARN 14938 --- [ main] o.s.b.d.r.c.RemoteClientConfiguration : The connection to http://localhost:8080 is insecure. You should use a URL starting with 'https://'.
- 2015-06-10 18:25:07.074 INFO 14938 --- [ main] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
- 2015-06-10 18:25:07.130 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Started RemoteSpringApplication in 0.74 seconds (JVM running for 1.105)
因为 classpath 是一样的, 所以可以直接读取真实的配置属性. 这就是 spring.devtools.remote.secret 发挥作用的时候了, Spring Boot 会用这个来认证.
建议使用 https:// 来连接, 这样密码会被加密, 不会被拦截.
如果你有一个代理服务器, 你需要设置 spring.devtools.remote.proxy.host 和 spring.devtools.remote.proxy.port 这两个属性.
远程更新
客户端会监控你的 classpath, 和本地重启的监控一样. 任何资源更新都会被推送到远程服务器上, 远程应用再判断是否触发了重启. 如果你在一个云服务器上做迭代, 这样会很有用. 一般来说, 字节更新远程应用, 会比你本地打包再发布要快狠多.
资源监控的前提是你启动了本地客户端, 如果你在启动之前修改了文件, 这个变化是不会推送到远程应用的.
远程 debug 通道
在定位和解决问题时, Java 远程调试是很有用的. 不幸的是, 如果你的应用部署在异地, 远程 debug 往往不是很容易实现. 而且, 如果你使用了类似 Docker 的容器, 也会给远程 debug 增加难度.
为了解决这么多困难, Spring Boot 支持在 HTTP 层面的 debug 通道. 远程应用汇提供 8000 端口来作为 debug 端口. 一旦连接建立, debug 信号就会通过 HTTP 传输给远程服务器. 你可以设置 spring.devtools.remote.debug.local-port 来改变默认端口. 你需要首先确保你的远程应用启动时已经开启了 debug 模式. 一般来说, 可以设置 JAVA_OPTS. 例如, 如果你使用的是 Cloud Foundry 你可以在 manifest.YAML 加入:
- env:
- JAVA_OPTS: "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n"
注意, 没有必要给 - Xrunjdwp 加上 address=NNNN 的配置. 如果不配置, Java 会随机选择一个空闲的端口. 远程 debug 是很慢的, 所以你最好设置好 debug 的超时时间(一般来说 60000 是足够了). 如果你使用 IntelliJ IDEA 来调试远程应用, 你一定要把所有断点设置成悬挂线程, 而不是悬挂 JVM. 默认情况, IDEA 是悬挂 JVM 的. 这个会造成很大的影响, 因为你的 session 会被冻结. 参考 IDEA-165769
来源: https://juejin.im/post/5befbd75f265da61671fe773