【Play】熱部署是如何工作的?

天府雲創發表於2018-01-12

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的,而其餘的不是。

參考

相關文章