拾贝

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

    backend     原创·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的,而其余的不是。

参考