1. 程式人生 > >Spring Devtools 原始碼初步解析

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 中的內部執行方法--------[email protected]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.spring[email protected]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