前言
最近在閱讀spring cloud源碼的時(shí)候 發(fā)現(xiàn)spring devtools這個(gè)包 覺(jué)得比較有趣,就研究了一下.然后寫(xiě)了這篇文章蕾管。
主要解決三個(gè)疑問(wèn)
1 如何初始化
2 如何實(shí)時(shí)監(jiān)聽(tīng)
3 如何遠(yuǎn)程重啟
1構(gòu)造
Restarter
Restarter是在spring容器啟動(dòng)過(guò)程中通過(guò)RestartApplicationListener接受ApplicationStartingEvent廣播然后進(jìn)行一系列初始化操作并實(shí)時(shí)監(jiān)聽(tīng)
首先RestartApplicationListener接受ApplicationStartingEvent事件廣播并判斷spring.devtools.restart.enabled是否開(kāi)啟如果開(kāi)啟就進(jìn)行初始化如下操作
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);
}
}
然后調(diào)用如下初始化方法
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方法中會(huì)再開(kāi)一個(gè)線程執(zhí)行this.start(FailureHandler.NONE)方法,這個(gè)方法會(huì)新起一個(gè)線程去初始化上下文,當(dāng)項(xiàng)目結(jié)束后再返回,如下代碼
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會(huì)啟動(dòng)RestartLauncher線程然后啟動(dòng)后就將當(dāng)前線程掛起,等待RestartLauncher線程任務(wù)完成。再來(lái)看看RestartLauncher線程執(zhí)行的任務(wù)
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線程會(huì)執(zhí)行啟動(dòng)類(lèi)的main方法相當(dāng)于重新創(chuàng)建應(yīng)用上下文
總結(jié)
由上面的流程可知當(dāng)?shù)谝淮螆?zhí)行的時(shí)候,如果沒(méi)有關(guān)閉spring developer那么就會(huì)創(chuàng)建Restarter并將當(dāng)前線程掛起然后重新起一個(gè)新的子線程來(lái)創(chuàng)建應(yīng)用上下文
2實(shí)時(shí)監(jiān)聽(tīng)
主要是通過(guò)類(lèi)FileSystemWatcher進(jìn)行實(shí)時(shí)監(jiān)聽(tīng)
首先啟動(dòng)過(guò)程如下
1 在構(gòu)建Application上下文的時(shí)候refreshContext創(chuàng)建bean的時(shí)候會(huì)掃描LocalDevToolsAutoConfiguration配置的ClassPathFileSystemWatcher進(jìn)行初始化 并同時(shí)初始化對(duì)應(yīng)依賴(lài) 如下圖
@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 然后會(huì)調(diào)用ClassPathFileSystemWatcher中InitializingBean接口所對(duì)應(yīng)的afterPropertiesSet方法去啟動(dòng)一個(gè)fileSystemWatcher ,在啟動(dòng)fileSystemWatcher的時(shí)候會(huì)在fileSystemWatcher上注冊(cè)一個(gè)ClassPathFileChangeListener監(jiān)聽(tīng)用于響應(yīng)監(jiān)聽(tīng)的目錄發(fā)生變動(dòng),具體代碼如下
@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內(nèi)部會(huì)啟動(dòng)一個(gè)Watcher線程用于循環(huán)監(jiān)聽(tīng)目錄變動(dòng),如果發(fā)生變動(dòng)就會(huì)發(fā)布一個(gè)onChange通知到所有注冊(cè)的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 中的內(nèi)部執(zhí)行方法-----------------------------------------------------------------------@Override
public void run() {
int remainingScans = this.remainingScans.get();
while (remainingScans > 0 || remainingScans == -1) {
try {
if (remainingScans > 0) {
this.remainingScans.decrementAndGet();
}
scan(); //監(jiān)聽(tīng)變動(dòng)并發(fā)布通知
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
remainingScans = this.remainingScans.get();
}
}
4 之前注冊(cè)的ClassPathFileChangeListener監(jiān)聽(tīng)器收到通知后會(huì)發(fā)布一個(gè)ClassPathChangedEvent(ApplicationEvent)事件,如果需要重啟就中斷當(dāng)前監(jiān)聽(tīng)線程。如下代碼
@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 上邊發(fā)布的ClassPathChangedEvent事件會(huì)被LocalDevToolsAutoConfiguration中配置的監(jiān)聽(tīng)器監(jiān)聽(tīng)到然后如果需要重啟就調(diào)用Restarter的方法進(jìn)行重啟 如下
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(
new FileWatchingFailureHandler(fileSystemWatcherFactory()));
}
}
3 LiveReload
liveReload用于在修改了源碼并重啟之后刷新瀏覽器
可通過(guò)spring.devtools.livereload.enabled = false 關(guān)閉
4 遠(yuǎn)程重啟
在查看devtools源碼的時(shí)候還有一個(gè)包(org.springframework.boot.devtools.remote)感覺(jué)挺有意思的,通過(guò)查資料得知,這個(gè)包可以用于遠(yuǎn)程提交代碼并重啟,所以研究了一下
因?yàn)閷?duì)這里的實(shí)際操作不太感興趣所有以下摘抄自 https://blog.csdn.net/u011499747/article/details/71746325
Spring Boot的開(kāi)發(fā)者工具不僅僅局限于本地開(kāi)發(fā)。你也可以應(yīng)用在遠(yuǎn)程應(yīng)用上履恩。遠(yuǎn)程應(yīng)用是可選的切心。如果你想開(kāi)啟片吊,你需要把devtools的包加到你的打包的jar中:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludeDevtools>false</excludeDevtools>
</configuration>
</plugin>
</plugins>
</build>
然后定鸟,你還需要設(shè)置一個(gè)遠(yuǎn)程訪問(wèn)的秘鑰spring.devtools.remote.secret:
spring.devtools.remote.secret=mysecret
開(kāi)啟遠(yuǎn)程開(kāi)發(fā)功能是有風(fēng)險(xiǎn)的著瓶。永遠(yuǎn)不要在一個(gè)真正的生產(chǎn)機(jī)器上這么用材原。
遠(yuǎn)程應(yīng)用支持兩個(gè)方面的功能余蟹;一個(gè)是服務(wù)端子刮,一個(gè)是客戶(hù)端。只要你設(shè)置了spring.devtools.remote.secret葵孤,服務(wù)端就會(huì)自動(dòng)開(kāi)啟尤仍∠烈蹋客戶(hù)端需要你手動(dòng)來(lái)開(kāi)啟。
運(yùn)行遠(yuǎn)程應(yīng)用的客戶(hù)端
遠(yuǎn)程應(yīng)用的客戶(hù)端被設(shè)計(jì)成在你的IDE中運(yùn)行赡模。你需要在擁有和你的遠(yuǎn)程應(yīng)用相同的classpath的前提下师抄,運(yùn)行org.springframework.boot.devtools.RemoteSpringApplication叨吮。這個(gè)application的參數(shù)就是你要連接的遠(yuǎn)程應(yīng)用的URL。
例如谚殊,如果你用的是Eclipse或者STS蛤铜,你有一個(gè)項(xiàng)目叫my-app,你已經(jīng)部署在云平臺(tái)上了蜂怎,你需要這么做:
- 從Run菜單選擇Run Configurations…
- 創(chuàng)建一個(gè)Java Application的啟動(dòng)配置
- 使用org.springframework.boot.devtools.RemoteSpringApplication作為啟動(dòng)類(lèi)
- 把https://myapp.cfapps.io作為程序的參數(shù)(這個(gè)URL是你真正的URL)
一個(gè)啟動(dòng)的遠(yuǎn)程應(yīng)用是這樣的:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \
\\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) )
' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / /
=========|_|==============|___/===================================/_/_/_/
:: 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)
因?yàn)閏lasspath是一樣的,所以可以直接讀取真實(shí)的配置屬性榜轿。這就是spring.devtools.remote.secret發(fā)揮作用的時(shí)候了谬盐,Spring Boot會(huì)用這個(gè)來(lái)認(rèn)證。
建議使用https://來(lái)連接皇型,這樣密碼會(huì)被加密砸烦,不會(huì)被攔截幢痘。
如果你有一個(gè)代理服務(wù)器雪隧,你需要設(shè)置spring.devtools.remote.proxy.host和spring.devtools.remote.proxy.port這兩個(gè)屬性。
遠(yuǎn)程更新
客戶(hù)端會(huì)監(jiān)控你的classpath藕畔,和本地重啟的監(jiān)控一樣庄拇。任何資源更新都會(huì)被推送到遠(yuǎn)程服務(wù)器上,遠(yuǎn)程應(yīng)用再判斷是否觸發(fā)了重啟溶弟。如果你在一個(gè)云服務(wù)器上做迭代辜御,這樣會(huì)很有用屈张。一般來(lái)說(shuō),字節(jié)更新遠(yuǎn)程應(yīng)用碳抄,會(huì)比你本地打包再發(fā)布要快狠多剖效。
資源監(jiān)控的前提是你啟動(dòng)了本地客戶(hù)端璧尸,如果你在啟動(dòng)之前修改了文件,這個(gè)變化是不會(huì)推送到遠(yuǎn)程應(yīng)用的。
遠(yuǎn)程debug通道
在定位和解決問(wèn)題時(shí)梦湘,Java遠(yuǎn)程調(diào)試是很有用的瞎颗。不幸的是,如果你的應(yīng)用部署在異地捌议,遠(yuǎn)程debug往往不是很容易實(shí)現(xiàn)哼拔。而且,如果你使用了類(lèi)似Docker的容器瓣颅,也會(huì)給遠(yuǎn)程debug增加難度倦逐。
為了解決這么多困難,Spring Boot支持在HTTP層面的debug通道宫补。遠(yuǎn)程應(yīng)用匯提供8000端口來(lái)作為debug端口檬姥。一旦連接建立,debug信號(hào)就會(huì)通過(guò)HTTP傳輸給遠(yuǎn)程服務(wù)器健民。你可以設(shè)置spring.devtools.remote.debug.local-port來(lái)改變默認(rèn)端口。
你需要首先確保你的遠(yuǎn)程應(yīng)用啟動(dòng)時(shí)已經(jīng)開(kāi)啟了debug模式贫贝。一般來(lái)說(shuō)秉犹,可以設(shè)置JAVA_OPTS。例如稚晚,如果你使用的是Cloud Foundry你可以在manifest.yml加入:
env:
JAVA_OPTS: "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n"
注意崇堵,沒(méi)有必要給-Xrunjdwp加上address=NNNN的配置。如果不配置客燕,Java會(huì)隨機(jī)選擇一個(gè)空閑的端口鸳劳。
遠(yuǎn)程debug是很慢的,所以你最好設(shè)置好debug的超時(shí)時(shí)間(一般來(lái)說(shuō)60000是足夠了)也搓。
如果你使用IntelliJ IDEA來(lái)調(diào)試遠(yuǎn)程應(yīng)用棍辕,你一定要把所有斷點(diǎn)設(shè)置成懸掛線程暮现,而不是懸掛JVM。默認(rèn)情況楚昭,IDEA是懸掛JVM的栖袋。這個(gè)會(huì)造成很大的影響,因?yàn)槟愕膕ession會(huì)被凍結(jié)抚太。參考IDEA-165769