最近在阅读spring cloud源码的时候 发现spring devtools这个包 觉得比较有趣,就研究了一下.然后写了这篇文章。
主要解决三个疑问 1 如何初始化 2 如何实时监听 3 如何远程重启
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并将当前线程挂起然后重新起一个新的子线程来创建应用上下文
主要是通过类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())); } } 复制代码
liveReload用于在修改了源码并重启之后刷新浏览器 可通过spring.devtools.livereload.enabled = false 关闭
在查看devtools源码的时候还有一个包(org.springframework.boot.devtools.remote)感觉挺有意思的,通过查资料得知,这个包可以用于远程提交代码并重启,所以研究了一下 因为对这里的实际操作不太感兴趣所有以下摘抄自 blog.csdn.net/u011499747/…
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,你已经部署在云平台上了,你需要这么做:
一个启动的远程应用是这样的:
. ____ _ __ _ _ /// / ___'_ __ _ _(_)_ __ __ _ ___ _ / / / / ( ( )/___ | '_ | '_| | '_ // _` | | _ /___ _ __ ___| |_ ___ / / / / /// ___)| |_)| | | | | || (_| []::::::[] / -_) ' // _ / _/ -_) ) ) ) ) ' |____| .__|_| |_|_| |_/__, | |_|_/___|_|_|_/___//__/___|/ / / / =========|_|==============|___/===================================/_/_/_/ :: 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,和本地重启的监控一样。任何资源更新都会被推送到远程服务器上,远程应用再判断是否触发了重启。如果你在一个云服务器上做迭代,这样会很有用。一般来说,字节更新远程应用,会比你本地打包再发布要快狠多。
资源监控的前提是你启动了本地客户端,如果你在启动之前修改了文件,这个变化是不会推送到远程应用的。
在定位和解决问题时,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