内容简介:最近在阅读spring cloud源码的时候 发现spring devtools这个包 觉得比较有趣,就研究了一下.然后写了这篇文章。主要解决三个疑问 1 如何初始化 2 如何实时监听 3 如何远程重启Restarter是在spring容器启动过程中通过RestartApplicationListener接受ApplicationStartingEvent广播然后进行一系列初始化操作并实时监听 首先RestartApplicationListener接受ApplicationStartingEvent事件
最近在阅读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
以上所述就是小编给大家介绍的《Spring Devtools 源码初步解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
An Introduction to the Analysis of Algorithms
Robert Sedgewick、Philippe Flajolet / Addison-Wesley Professional / 1995-12-10 / CAD 67.99
This book is a thorough overview of the primary techniques and models used in the mathematical analysis of algorithms. The first half of the book draws upon classical mathematical material from discre......一起来看看 《An Introduction to the Analysis of Algorithms》 这本书的介绍吧!