转载

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)感觉挺有意思的,通过查资料得知,这个包可以用于远程提交代码并重启,所以研究了一下 因为对这里的实际操作不太感兴趣所有以下摘抄自 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,你已经部署在云平台上了,你需要这么做:

  • 从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

原文  https://juejin.im/post/5befbd75f265da61671fe773
正文到此结束
Loading...