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

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值