Spring Devtools 源码初步解析

前言

最近在阅读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)感觉挺有意思的,通过查资料得知,这个包可以用于远程提交代码并重启,所以研究了一下
因为对这里的实际操作不太感兴趣所有以下摘抄自 https://blog.csdn.net/u011499747/article/details/71746325


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.yml加入:

    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


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • 匆匆忙忙又回到了家里。从闭塞封闭的高三教室回到了时刻不在变化的花花世界里,从冷酷激烈的高三教室回到了温暖安静的...
    名字被人抢走了啊阅读 259评论 0 0
  • 繁华尘世中,谁能为我织就一帘幽梦。 梦里的山清水秀,云淡风清, 梦里有幽幽弯月,点点繁星。 春有时鸣山涧的飞鸟, ...
    陌上花开V缓缓归矣阅读 284评论 0 0
  • 引言 机器学习研究的主要内容是关于在计算机上从数据中产生“模型”(model)的算法,即学习算法(learning...
    奋进的小毛驴阅读 459评论 0 0