【Play】熱部署是如何工作的?
1.什麼是熱部署
所謂熱部署,就是在應用正在執行的時候升級軟體,卻不需要重新啟動應用。對於Java應用程式來說,熱部署就是在執行時更新Java類檔案。– 百度百科
對於Java應用,有三種常見的實現熱部署的方式:
- JPDA: 利用JVM原生的JPDA介面,參見官方文件
- Classloader: 通過建立新的Classloader來載入新的Class檔案。OSGi就是通過這種方式實現Bundle的動態載入。
- Agent: 通過自定義Java Agent實現Class動態載入。JRebel,hotswapagent使用的就是這種方式。
Play console自帶的auto-reload功能正是基於上述第二種方式實現的。
2.Auto-reload機制
Play console是Typesafe封裝的一種特殊的的sbt console,主要增加了activator new和activator ui兩個命令。其auto-reload功能是以sbt外掛(”com.typesafe.play” % “sbt-plugin”)的形式提供的,sbt-plugin通過sbt-run-support類庫連線到play開發模式下的啟動類(play.core.server.DevServerStart)。每當應用收到請求時,play會通過sbt-plugin檢查是否有原始檔被修改,如果存在,則呼叫sbt命令進行編譯,然後依次停止老的play應用,建立新的classloader,然後啟動新的play應用,在此過程中執行sbt的JVM並沒有被重啟,只是play應用完成了重啟。
3.原始碼分析
以下分別從sbt-plugin,sbt-run-support和play-server挑選3個核心類對上述流程進行簡單梳理。
play.sbt.run.PlayRun
定義play run task,通過Reloader傳遞sbt回撥函式引用給DevServerStart。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[Line 73-93: PlayRun#playRunTask] lazy val devModeServer = Reloader.startDevMode( runHooks.value, (javaOptions in Runtime).value, dependencyClasspath.value.files, dependencyClassLoader.value, reloadCompile, # sbt回撥函式引用 reloaderClassLoader.value, assetsClassLoader.value, playCommonClassloader.value, playMonitoredFiles.value, fileWatchService.value, (managedClasspath in DocsApplication).value.files, playDocsJar.value, playDefaultPort.value, playDefaultAddress.value, baseDirectory.value, devSettings.value, args, runSbtTask, (mainClass in (Compile, Keys.run)).value.get ) |
play.runsupport.Reloader
通過反射啟動play應用,將Reloader自身作為引數傳入。
1 2 3 4 5 6 7 8 9 10 11 |
[Line 203-212: Reloader#startDevMode] val server = { val mainClass = applicationLoader.loadClass(mainClassName) if (httpPort.isDefined) { val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[BuildDocHandler], classOf[Int], classOf[String]) mainDev.invoke(null, reloader, buildDocHandler, httpPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ServerWithStop] } else { val mainDev = mainClass.getMethod("mainDevOnlyHttpsMode", classOf[BuildLink], classOf[BuildDocHandler], classOf[Int], classOf[String]) mainDev.invoke(null, reloader, buildDocHandler, httpsPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ServerWithStop] } } |
play.core.server.DevServerStart
從註釋可以清楚的看到stop-and-start的重啟邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
[Line 113-180: DevServerStart#mainDev] val reloaded = buildLink.reload match { case NonFatal(t) => Failure(t) case cl: ClassLoader => Success(Some(cl)) case null => Success(None) } reloaded.flatMap { maybeClassLoader => val maybeApplication: Option[Try[Application]] = maybeClassLoader.map { projectClassloader => try { if (lastState.isSuccess) { println() println(play.utils.Colors.magenta("--- (RELOAD) ---")) println() } val reloadable = this // First, stop the old application if it exists lastState.foreach(Play.stop) // Create the new environment val environment = Environment(path, projectClassloader, Mode.Dev) val sourceMapper = new SourceMapper { def sourceOf(className: String, line: Option[Int]) = { Option(buildLink.findSource(className, line.map(_.asInstanceOf[java.lang.Integer]).orNull)).flatMap { case Array(file: java.io.File, null) => Some((file, None)) case Array(file: java.io.File, line: java.lang.Integer) => Some((file, Some(line))) case _ => None } } } val webCommands = new DefaultWebCommands currentWebCommands = Some(webCommands) val newApplication = Threads.withContextClassLoader(projectClassloader) { val context = ApplicationLoader.createContext(environment, dirAndDevSettings, Some(sourceMapper), webCommands) val loader = ApplicationLoader(context) loader.load(context) } Play.start(newApplication) Success(newApplication) } catch { case e: PlayException => { lastState = Failure(e) lastState } case NonFatal(e) => { lastState = Failure(UnexpectedException(unexpected = Some(e))) lastState } case e: LinkageError => { lastState = Failure(UnexpectedException(unexpected = Some(e))) lastState } } } maybeApplication.flatMap(_.toOption).foreach { app => lastState = Success(app) } maybeApplication.getOrElse(lastState) } |
4. Gotcha
上述的實現看上去並不複雜,那為什麼老牌的Tomcat,JBoss容器卻始終沒有提供類似的機制呢?原因很簡單,Play是stateless的,而其餘的不是。
參考
相關文章
- 熱門 Python 應用 The Fuck 是如何工作的Python
- DNS是如何工作的?DNS
- Cucumber是如何工作的?
- Javascript是如何工作的JavaScript
- Orchard是如何工作的?
- CDN是如何工作的?
- instanceof 是如何工作的
- 代理IP是如何工作的?
- 代理API是如何工作的?API
- 反向代理是如何工作的?
- SOCKS代理是如何工作的?
- webpack HMR是如何工作的?Web
- webpack的require是如何工作的?WebUI
- 解密BGPaaS代理是如何工作的解密
- TCP和UDP是如何工作的TCPUDP
- vue-cli是如何工作的Vue
- 編譯器是如何工作的?編譯
- Java垃圾回收是如何工作的?Java
- Webpack的熱更新是如何做到的?原理是什麼?Web
- ERP系統是如何工作的
- 你知道SSL是如何工作的嗎?
- 代理伺服器是如何工作的?伺服器
- 神經網路是如何工作的?神經網路
- Eclipse/tomcat 如何實現應用熱部署和熱啟動EclipseTomcat熱部署
- 區塊鏈技術是如何工作的區塊鏈
- MacBook Pro 高功率模式:是如何工作的?Mac模式
- 淺嘗輒止,React是如何工作的React
- 【譯】Arc 在 Rust 中是如何工作的Rust
- 什麼是代理以及它是如何工作的?
- Spring MVC 到底是如何工作的?SpringMVC
- Java I/O底層是如何工作的?Java
- 我是如何在Stack Overflow找到工作的
- Facebook 工程師是如何高效工作的?工程師
- Facebook工程師是如何高效工作的?工程師
- 單執行緒的js是如何工作的執行緒JS
- 說說webpack的熱更新是如何做到的?原理是什麼?Web
- idea熱部署Idea熱部署
- eclipse 熱部署Eclipse熱部署