Skip to content

Instantly share code, notes, and snippets.

@tanacasino
Created December 7, 2016 07:55
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanacasino/d031ab31923a409ec307b77cc1b29924 to your computer and use it in GitHub Desktop.
Save tanacasino/d031ab31923a409ec307b77cc1b29924 to your computer and use it in GitHub Desktop.
play2-src-reading
<style> pre { font-size: 30px; } </style>

こわくないよ ❤️ Playframework ソースコードリーディング 入門


本日のお品書き

  1. ☕ 前段
  2. 🍅 ソースコードツリーを眺めよう!
  3. 🍖 起動処理を追いかけよう!
  4. 🍰 リクエストを受け取ってレスポンスを返すまで
謝罪

残念ながら時間不足の予感です。3と4で区切りますので、お時間ある方のみ 4 までどうぞ。 3までは予定通り 20:00で完了予定です。


前段


ワタシダレ 👨

  • 田中智文 タナカトモフミ @tanacasino
  • 高知県高知市百石町から上京中
  • 二児の父 👶 👶
  • 株式会社ビズリーチ ターミナル部 部長
  • Scalaを業務で2年ほど。その前はEucalytpus、OpenStack、スクラムなどなど
  • GitBucket の幽霊部員(Committer)

進め方

  • スライドでナビしますので、手元でソースコードを開いて読んでみてスタイル
  • 随時気になる点はアピってください。顔で進捗を表現していただければペースを調整できる可能性があります 😉
  • スライドのソースは長さの問題で省略版なので、手元のソースコードを正としてみてください
  • スライド見づらい会場っぽいので、アップロードしました。ダウンロードして手元でもご覧下さい
  • スライドにGitHubのソースコードへのリンク作ってますので、迷子になったらクリック

本題


🌲 ソースコードツリーを眺めよう!


リポジトリのトップ

  • documentation
    • 公式ドキュメントのソース
  • framework
    • フレームワークの本体ソースコード
    • 今日のメイン
  • templates
    • Playのプロジェクトの雛形
    • Lightbend社のHPからダウンロードできる雛形のソースコード play-scala など

🌲 ソースコードツリー 🌲

build-link            play-functional       play-netty-utils
fork-run              play-integration-test play-server
fork-run-protocol     play-java             play-specs2
iteratees             play-java-jdbc        play-streams
play                  play-java-jpa         play-test
play-akka-http-server play-java-ws          play-ws
play-cache            play-jdbc             routes-compiler
play-datacommons      play-jdbc-api         run-support
play-docs             play-jdbc-evolutions  sbt-fork-run-plugin
play-docs-sbt-plugin  play-json             sbt-plugin
play-exceptions       play-logback
play-filters-helpers  play-netty-server

🚿 とりあえずざざっと流す


どこから読むか???

  1. 適当にえいや!で決める
  2. ActionとかControllerとか使うとすぐに出てるところから読む
  3. Jsonとか簡単そうなところから始める
  4. Applicativeとかモナモナしてるところから始める


main文でしょ 😘

def main(args: Array[String]): Unit

main文を探そう 👀

  • ag 'def main\(' src
    src/fork-run/src/main/scala/play/forkrun/ForkRun.scala
    28:  def main(args: Array[String]): Unit = {
    
    src/play-netty-server/src/main/scala/play/core/server/NettyServer.scala
    298:  def main(args: Array[String]) {
    
    src/play-server/src/main/scala/play/core/server/ProdServerStart.scala
    20:  def main(args: Array[String]) {
    
  • 3つしかない 👍
  • fork-run はsbtのやつなので関係なさそう

  def main(args: Array[String]) {
    System.err.println(
      s"NettyServer.main is deprecated. \
        Please start your Play server with the \
        ${ProdServerStart.getClass.getName}.main."
    )
    ProdServerStart.main(args)
  }
  • ProdServerStart を使え!とのことです

念のため確認する

  • main文のgrepは乱暴すぎたかも
  • play-scala
  • sbt dist したパッケージのシェルスクリプトも
    declare -a app_mainclass=("play.core.server.ProdServerStart")
    # ざっくり言うと java -jar play.core.server.ProdServerStart してる感じ
  • ProdServerStart を実行してるだけ!!!

起動処理を読んでみよう!

object ProdServerStart {
  /**
   * Start a prod mode server from the command line.
   */
  def main(args: Array[String]) {
    val process = new RealServerProcess(args)
    start(process)
  }
  // ...
}

  • ServerProcess はJVMプロセスを抽象化してテスト用にモックで差し替えれるようにしてるよ!
/**
 * Abstracts a JVM process so it can be mocked for testing or to
 * isolate pseudo-processes within a VM. Code using this class
 * should use the methods in this class instead of methods like
 * `System.getProperties()`, `System.exit()`, etc.
 */
trait ServerProcess

class RealServerProcess(
  val args: Seq[String]
) extends ServerProcess

  • クラスローダや起動時の引数、プロセスのシャットダウンフックを提供
trait ServerProcess {
  /** The ClassLoader that should be used */
  def classLoader: ClassLoader
  /** The command line arguments the process as invoked with */
  def args: Seq[String]
  /** The process's system properties */
  def properties: Properties
  /** Helper for getting properties */
  final def prop(name: String): Option[String] = Option(properties.getProperty(name))
  /** The process's id */
  def pid: Option[String]
  /** Add a hook to run when the process shuts down */
  def addShutdownHook(hook: => Unit): Unit
  /** Exit the process with a message and optional cause and return code */
  def exit(message: String, cause: Option[Throwable] = None, returnCode: Int = -1): Nothing
}

  • pidってそうやって取れるんだーって感想のみ
class RealServerProcess(val args: Seq[String]) extends ServerProcess {
  def classLoader: ClassLoader =
  	Thread.currentThread.getContextClassLoader    
  def properties: Properties = System.getProperties
  def pid: Option[String] = ManagementFactory.getRuntimeMXBean
                                 .getName.split('@').headOption
  def addShutdownHook(hook: => Unit): Unit = {
    Runtime.getRuntime.addShutdownHook(new Thread {
      override def run() = hook
    })
  }
  def exit(message: String, cause: Option[Throwable] = None, returnCode: Int = -1): Nothing = {
    /* ... */
  }
}

戻ってきて ProdServerStart#start

def main(args: Array[String]): Unit = {
  val process = new RealServerProcess(args)
  // イマココ
  start(process)
}
// ざっくり4ステップ
def start(process: ServerProcess): ServerWithStop = {
  try {
    // 1. Read settings
    val config: ServerConfig = readServerConfigSettings(process)
    // 2. Create a PID file before we do any real work
    val pidFile = createPidFile(process, config.configuration)
    // 3. Start the application
    val application: Application = { /**/ }
    Play.start(application)
    // 4. Start the server
    val serverProvider: ServerProvider = ServerProvider.fromConfiguration(process.classLoader, config.configuration)
    val server = serverProvider.createServer(config, application)
    // ... 省略
}

ProdServerStart#start のやってること

  1. コンフィグの読み込み
  2. PID(Process ID) ファイルを作成する
  3. play.api.Application を作って初期化する
  4. HTTPサーバを起動する

1. コンフィグの読み込み src

  • 特に難しくないですが、
  • !(option).isDefined(option).isEmpty にしたい
def readServerConfigSettings(process: ServerProcess): ServerConfig = {
  val configuration: Configuration = { /* 設定ファイルの読み込み */ }
  val rootDir: File = { /* アプリのデプロイされたディレクトリ */ }
  def parsePort(portType: String): Option[Int] = {
    // configurationから http/httpsのポート番号をparseする
  }
  val httpPort = parsePort("http")
  val httpsPort = parsePort("https")
  if (!(httpPort orElse httpsPort).isDefined) throw ServerStartException("Must provide either an HTTP or HTTPS port")
  val address = configuration.getString("play.server.http.address")
                             .getOrElse("0.0.0.0")
  ServerConfig(/**/)
}

  • {} で囲んでブロックにすることで rootDirArg等の一時変数のスコープを狭くしてる
val configuration: Configuration = {
  // コマンドライン引数でrootDir が指定されている場合はSome(java.io.File)。ない場合はNone
  val rootDirArg: Option[File] =
        process.args.headOption.map(new File(_))
  // コマンドライン引数で指定がない場合は、Map.emptyを返す。
  // 指定がある場合 play.server.dir の値を、引数で指定されたものにする
  // "play.server.dir" -> "${dir.getAbsolutePath}"のMapを返す
  val rootDirConfig = rootDirArg.fold(Map.empty[String, String])
  (dir => ServerConfig.rootDirConfig(dir))
  // Configurationを読み込む.
  Configuration.load(
    process.classLoader, process.properties, rootDirConfig, true
  )
}

  • いろいろ読み込んでる
object Configuration {
  private[play] def load(
    classLoader: ClassLoader,
    properties: Properties,
    directSettings: Map[String, AnyRef],
    allowMissingApplicationConf: Boolean): Configuration = {
    try {
      // 1. SystemPropertyからの設定
      val systemPropertyConfig = /* 省略 */
      // 2. Map[String, String]で指定された設定
      val directConfig: Config = ConfigFactory.parseMap(directSettings.asJava)
      // 3. conf/application.confの設定
      val applicationConfig: Config = /* 省略 */
      // 4. play overrides conf
      val playOverridesConfig: Config = ConfigFactory.parseResources(classLoader, "play/reference-overrides.conf")
      // 5. reference.conf
      val referenceConfig: Config = ConfigFactory.parseResources(classLoader, "reference.conf")
     // ... 省略

設定のインプットが5種類

  1. SystemProperty (System.getProperty)
  2. DirectConfig: コマンドライン引数
  3. アプリの設定(conf/application.confなど)
  4. play overrides config
    • Akka等の外部ライブラリの設定をPlayが推奨する値で上書き
  5. reference.conf
    • Playによるデフォルトの設定値

いろいろある設定を Combine !!!

  • reduceLeft(_ withFallback _) がカッコイイ ⚡
      // Combine all the config together into one big config
      val combinedConfig: Config = Seq(
        systemPropertyConfig,
        directConfig,
        applicationConfig,
        playOverridesConfig,
        referenceConfig
      ).reduceLeft(_ withFallback _)

      // resolve すると ${foo.bar} のような
      // 環境変数などで置き換えを想定している項目が解決される
      val resolvedConfig = combinedConfig.resolve
      Configuration(resolvedConfig)
    } catch {
      case e: ConfigException => throw configError(e.origin, e.getMessage, Some(e))
    }

  • 必ず必要(存在する)な場合には、Option.get ではなく getOrElse(throw new Exception) パターンで、正しいエラーを出そう
val rootDir: File = {
  // 引数で指定されていた場合はplay.server.dirは引数で渡された値になっている
  val path = configuration
      .getString("play.server.dir")
      .getOrElse(throw ServerStartException("No root server path supplied"))
  val file = new File(path)
  if (!(file.exists && file.isDirectory)) {
    throw ServerStartException(s"Bad root server path: $path")
  }
  file
}

  • http/https のいずれかのポート番号は必須
def parsePort(portType: String): Option[Int] = {
  configuration.getString(s"play.server.${portType}.port").flatMap {
    case "disabled" => None
    case str =>
      val i = try Integer.parseInt(str) catch {
        case _: NumberFormatException => throw ServerStartException(s"Invalid ${portType.toUpperCase} port: $str")
      }
      Some(i)
  }
}

val httpPort = parsePort("http")
val httpsPort = parsePort("https")
if (!(httpPort orElse httpsPort).isDefined) throw ServerStartException("Must provide either an HTTP or HTTPS port")

ここまでの学び

  • TypeSafe Config の知識が必要
  • 設定のインプットになるものが5種類ほど
  • reduceLeft でCombine かっこいい
  • 一時変数は {} で囲った中で使ってスコープを狭くする
  • Option.get せずに getOrElse(throw new Exception) すべし

2. PIDファイルを作成する src

// 2. Create a PID file before we do any real work
val pidFile = createPidFile(process, config.configuration)

def createPidFile(process: ServerProcess, configuration: Configuration): Option[File] = {
  // "play.server.pidfile.path" の設定がない場合は例外
  val pidFilePath = configuration
                      .getString("play.server.pidfile.path")
                      .getOrElse(throw ServerStartException("Pid file path not configured"))
  // "play.server.pidfile.path" が "/dev/null" の場合は作らない
  if (pidFilePath == "/dev/null") None else {
    val pidFile = new File(pidFilePath).getAbsoluteFile
    // PIDファイルがすでにある場合は例外
    if (pidFile.exists) {
      throw ServerStartException(s"This application is already running (Or delete ${pidFile.getPath} file).")
    }
    val pid = process.pid getOrElse (throw ServerStartException("Couldn't determine current process's pid"))
    val out = new FileOutputStream(pidFile)
    try out.write(pid.getBytes) finally out.close()
    Some(pidFile)
  }
}

3. Applicationインスタンスを作る src

  • play.api.Application はUTでも使ってるやつです
// 3. Start the application
val application: Application = {
  val environment = Environment(config.rootDir,
                                process.classLoader,
                                Mode.Prod)
  val context = ApplicationLoader.createContext(environment)
  val loader = ApplicationLoader(context)
  loader.load(context)
}
Play.start(application)
  • パッと見は簡単そうなコードですがここからが結構長いんです 😓
  • 一回水飲みましょう 🚰

  • なんてことないcase class
  • 開発時と本番で使うものが異なる場合などに参照
/**
 * The environment for the application.
 *
 * Captures concerns relating to the classloader and 
 * the filesystem for the application.
 *
 * @param アプリのデプロイされてるディレクトリ
 * @param classLoader アプリのクラスローダ
 * @param mode モード(Prod/Dev/Testの3モード)
 */
case class Environment(
  rootPath: File,
  classLoader: ClassLoader,
  mode: Mode.Mode
)

/**
 * Create an application loading context.
 * Locates and loads the necessary configuration files for the application.
 */
def createContext(environment: Environment,
  initialSettings: Map[String, AnyRef] = Map.empty[String, AnyRef],
  sourceMapper: Option[SourceMapper] = None,
  webCommands: WebCommands = new DefaultWebCommands) = {
  val configuration = Configuration.load(environment, initialSettings)
  Context(environment, sourceMapper, webCommands, configuration)
}
  • SourceMapper: エラーページでソースコードを表示する機能のもの
  • WebCommands: Evolutionsのスクリプトを適用する時のあれ

  • Scala/Java 両方をサポートするためやや複雑
// Locate and instantiate the ApplicationLoader.
def apply(context: Context): ApplicationLoader = {
  Reflect.configuredClass[
    ApplicationLoader, play.ApplicationLoader, GuiceApplicationLoader
  ](
    context.environment,
    PlayConfig(context.initialConfiguration),
    "play.application.loader",
    classOf[GuiceApplicationLoader].getName
  ) match {
      case None => /* デフォルト */
        new GuiceApplicationLoader  /* 今日はココ */
      case Some(Left(scalaClass)) => /* 独自の設定 Scala */
        scalaClass.newInstance
      case Some(Right(javaClass)) => /* 独自の設定 Java */
        javaClass.newInstance
    }
}

ApplicationLoader 補足

  • ApplicationLoaderは独自のクラスに置き換えできる
  • Playでは Runtime DIとCompile time DIをサポートしている
  • play.application.loaderApplicationLoaderを継承したクラスを設定することで、Compile time DIも使用可能
    • Scalaの場合は、traitをJavaの場合はinterfaceを
  • 今回は GuiceによるRuntime DIを使う場合(デフォルト)のものをリーディングしていきます

  • Guiceを使ってアプリをブートするマン
  • def this() はデフォルトコンストラクタ
  • リフレクション用にデフォルトコンストラクタ
/**
 * An ApplicationLoader that uses Guice to bootstrap the application.
 *
 * Subclasses can override the `builder` and `overrides` methods.
 */
class GuiceApplicationLoader(
  protected val initialBuilder: GuiceApplicationBuilder
) extends ApplicationLoader {

  // empty constructor needed for instantiating via reflection
  def this() = this(new GuiceApplicationBuilder)
}

  • ApplicationをGuice使って作るマン
  • フィールド多い。。。
final case class GuiceApplicationBuilder(
  environment: Environment = Environment.simple(),
  configuration: Configuration = Configuration.empty,
  modules: Seq[GuiceableModule] = Seq.empty,
  overrides: Seq[GuiceableModule] = Seq.empty,
  disabled: Seq[Class[_]] = Seq.empty,
  binderOptions: Set[BinderOption] = BinderOption.defaults,
  eagerly: Boolean = false,
  loadConfiguration: Environment => Configuration = Configuration.load,
  global: Option[GlobalSettings.Deprecated] = None,
  loadModules: (Environment, Configuration) => Seq[GuiceableModule] = GuiceableModule.loadModules
) extends GuiceBuilder[GuiceApplicationBuilder](
  environment, configuration, modules, overrides, disabled, binderOptions, eagerly
)

迷子になりそうなので戻って確認 src

  • loader は GuiceApplicationLoader
  • loader は GuiceApplicationBuilder を持っている
  • 次は loader.load!
// Start the application
val application: Application = {
  val environment = Environment(config.rootDir, process.classLoader, Mode.Prod)
  val context = ApplicationLoader.createContext(environment)
  val loader = ApplicationLoader(context)
  // イマココ
  loader.load(context)
}

  • GuiceApplicationBuilderのbuildを呼ぶ
  • builderにConfigやEnvironmentを渡す
override final def load(
  context: ApplicationLoader.Context
): Application = {
  builder(context).build
}
protected def builder(
  context: ApplicationLoader.Context
): GuiceApplicationBuilder = {
  initialBuilder.disableCircularProxies()
    .in(context.environment)
    .loadConfig(context.initialConfiguration)
    .overrides(overrides(context): _*)
}
protected def overrides(context: ApplicationLoader.Context): Seq[GuiceableModule] = {
  GuiceApplicationLoader.defaultOverrides(context)
}

  • GuiceのModule(インスタンスを指定)を設定
object GuiceApplicationLoader {
  /**
   * The default overrides provided by the Scala and Java GuiceApplicationLoaders.
   */
  def defaultOverrides(
    context: ApplicationLoader.Context
  ): Seq[GuiceableModule] = {
    Seq(
      bind[OptionalSourceMapper] to new OptionalSourceMapper(context.sourceMapper),
      bind[WebCommands] to context.webCommands,
      bind[DefaultApplicationLifecycle] to context.lifecycle)
  }
}

  • injectorを使ってApplicationインスタンスを生成している
  • injector(): play.api.injector.PlayInjector です
  • def injector で GuiceのInjectorを作ってる予感
override final def load(
  context: ApplicationLoader.Context
): Application = {
  builder(context).build // ココ
}

/**
 * Create a new Play Application using this configured builder.
 */
def build(): Application = injector().instanceOf[Application]

GuiceBuilder#injector (親クラス)

  • GuiceApplicationBuilderの親はGuiceBuilder
  • ついにGuiceのInjectorが登場 🍹
  /**
   * Create a Play Injector backed by Guice using this configured builder.
   */
  def injector(): PlayInjector = {
    try {
      val stage = /**/
      // Injector(com.google.inject.Injector)登場!
      val guiceInjector =
        Guice.createInjector(stage, applicationModule())
      
      // Injectorが PlayInjector を作っている!!!
      guiceInjector.getInstance(classOf[PlayInjector])
    } catch {
      // ...
    }
  }

  • PlayInjectorは GuiceInjector
  • conf等で有効化されたModuleも読み込む
  // Create a Play Injector backed by Guice using this configured builder.
  def applicationModule(): GuiceModule = createModule()

  def createModule(): GuiceModule = {
    import scala.collection.JavaConverters._
    val injectorModule = GuiceableModule.guice(Seq(
      bind[PlayInjector].to[GuiceInjector],
      bind[play.inject.Injector].to[play.inject.DelegateInjector]
    ), binderOptions)
    val enabledModules = modules.map(_.disable(disabled))
    val bindingModules = GuiceableModule.guiced(environment, configuration, binderOptions)(enabledModules) :+ injectorModule
    val overrideModules = GuiceableModule.guiced(environment, configuration, binderOptions)(overrides)
    GuiceModules.`override`(bindingModules.asJava).`with`(overrideModules.asJava)
  }

class GuiceInjector @Inject() (
  injector: com.google.inject.Injector
) extends PlayInjector {
  def instanceOf[T](implicit ct: ClassTag[T]) = instanceOf(ct.runtimeClass.asInstanceOf[Class[T]])
  def instanceOf[T](clazz: Class[T]) = injector.getInstance(clazz)
  def instanceOf[T](key: BindingKey[T]) = injector.getInstance(GuiceKey(key))
}
  • GuiceInjectorは単純に GuiceのInjectorを持ってるだけ
  • instanceOf[T] でインスタンスを生成・取得できるメソッド
  • injector().instanceOf[Application] の正体に辿りついたけど。。。

Application のインスタンスの生成方法はどこ?

play {
  modules {
    # The enabled modules that should be automatically loaded.
    enabled += "play.api.inject.BuiltinModule" # コイツだよ!
    enabled += "play.api.i18n.I18nModule"

    # A way to disable modules that are automatically enabled
    disabled = []
  }
}

  • 見たことあるやつ大体います。大体大事だけどすべてのProviderに目を通すのは暇なときにどうぞ!
class BuiltinModule extends Module {
  def bindings(env: Environment, configuration: Configuration): Seq[Binding[_]] = {
    // ... 省略 ...
    Seq(
      bind[Environment] to env,
      bind[Configuration].toProvider[ConfigurationProvider],
      bind[DefaultApplicationLifecycle].toSelf,
      bind[ApplicationLifecycle].to(bind[DefaultApplicationLifecycle]),

      bind[Application].to[DefaultApplication], // こいつだ!!!
      bind[play.Application].to[play.DefaultApplication],
      bind[ActorSystem].toProvider[ActorSystemProvider],
      bind[Materializer].toProvider[MaterializerProvider],
      bind[ExecutionContext].to[ExecutionContextExecutor],
     // ... 省略 ...
}

  • シンプル ☺️
  • Injectするフィールドも見たことありますね!
@Singleton
class DefaultApplication @Inject() (
    environment: Environment,
    applicationLifecycle: DefaultApplicationLifecycle,
    override val injector: Injector,
    override val configuration: Configuration,
    override val requestHandler: HttpRequestHandler,
    override val errorHandler: HttpErrorHandler,
    override val actorSystem: ActorSystem,
    override val materializer: Materializer) extends Application {

  def path = environment.rootPath
  def classloader = environment.classLoader
  def mode = environment.mode
  def stop() = applicationLifecycle.stop()
}

お、覚えてますか? src

  • 長い戦いですが、起きてますか? 😪
// 3. Start the application
val application: Application = {
  val environment = Environment(config.rootDir,
                                process.classLoader,
                                Mode.Prod)
  val context = ApplicationLoader.createContext(environment)
  val loader = ApplicationLoader(context)
  loader.load(context)
}
// イマココ!!!
Play.start(application)

  @volatile private[play] var _currentApp: Application = _
  def start(app: Application) {
    stop(_currentApp)
    _currentApp = app
    Threads.withContextClassLoader(app.classloader) {
      app.global.beforeStart(app)
      app.routes
      app.global.onStart(app)
    }
    app.mode match {
      case Mode.Test =>
      // やりました!startedですよ!
      case mode => logger.info("Application started (" + mode + ")")
    }
  }
  • 過去の互換性もあり怪しい感じですが、見たことある起動メッセージの表示まで来ました 🎉

Application まとめ

  • ApplicationLoader を使って Application を作る
  • ApplicationLoader は差し替え可能
  • デフォルトではGuiceを使うので以下を覚えておけば良さそう
    • GuiceApplicationLoader
    • GuiceApplicationBuilder
    • BuiltinModules
    • DefaultApplication

あれ?まだHTTPサーバが起動してないのでは?

  • そうです。これからnettyです。 src
  • ServerProviderを使ってServerを作ってるだけ
  • プロセスの終了処理時のhookでサーバを停止して、PIDファイルを削除
val serverProvider: ServerProvider =
  ServerProvider.fromConfiguration(
    process.classLoader, config.configuration
  )
val server = serverProvider.createServer(config, application)
process.addShutdownHook {
  server.stop()
  pidFile.foreach(_.delete())
  assert(!pidFile.exists(_.exists), "PID file should not exist!")
}
server

def fromConfiguration(classLoader: ClassLoader, configuration: Configuration): ServerProvider = {
  val ClassNameConfigKey = "play.server.provider"
  val className: String = configuration.getString(ClassNameConfigKey).getOrElse(throw new ServerStartException(/**/))
  val clazz = try classLoader.loadClass(className) catch { // 省略 }
  val ctor = try clazz.getConstructor() catch { // 省略 }
  ctor.newInstance().asInstanceOf[ServerProvider]
}

  • play.core.server.NettyServer をnewするだけの簡単なお仕事!
  def createServer(context: ServerProvider.Context) =
    new NettyServer(
      context.config,
      context.appProvider,
      context.stopHook,
      context.actorSystem
    )(
      context.materializer
    )

  • 特に何もしていないフリをして、val の宣言でbindしてるのでnewするだけで起動 src
class NettyServer(/**/) {
  // Maybe the HTTP server channel
  private val httpChannel =
  	config.port.map(bindChannel(_, secure = false))
  // Maybe the HTTPS server channel
  private val httpsChannel =
  	config.sslPort.map(bindChannel(_, secure = true))
    
  private def bindChannel(port: Int, secure: Boolean): Channel = {
    val protocolName = if (secure) "HTTPS" else "HTTP"
    val address = new InetSocketAddress(config.address, port)
    val (serverChannel, channelSource) = bind(address)
    channelSource.runWith(channelSink(port = port, secure = secure))
    /* 省略 */
  }
}

  • nettyとakka-stream繋いでSourceを作る
  • HTTPリクエストのインプットをSourceになるようにしていると思えばOK
  • Source => Sink で処理
  private def bind(address: InetSocketAddress): (Channel, Source[Channel, _]) = {
    val serverChannelEventLoop = eventLoop.next

    // Watches for channel events, and pushes them through a reactive streams publisher.
    val channelPublisher = new HandlerPublisher(serverChannelEventLoop, classOf[Channel])
    /**/
    val channel = bootstrap.bind.await().channel()
    allChannels.add(channel)

    (channel, Source.fromPublisher(channelPublisher))
  }

  • input(Source)を処理するSinkを作る
  • PlayRequestHandler が アプリの処理
val (serverChannel, channelSource) = bind(address)
// イマココ
channelSource.runWith(channelSink(port = port, secure = secure))
// 注意: いろいろ省略版です。詳細版はソースコード見てね
private def channelSink(port: Int, secure: Boolean): Sink[Channel, Future[Done]] = {
  Sink.foreach[Channel] { (connChannel: Channel) =>
    val pipeline = connChannel.pipeline() /* ... */
    pipeline.addLast("decoder", new HttpRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize))

    val requestHandler = new PlayRequestHandler(this) // コレ大事!

    pipeline.addLast("http-handler", new HttpStreamsServerHandler(Seq[ChannelHandler](requestHandler).asJava))
    pipeline.addLast("request-handler", requestHandler)   
    /* ... */
  }
}

NettyServerのポイント

  • akka-stream と netty の世界とつないでます (Source/Sink)
  • play.server.netty.transportnative を指定するとepoll使ってくれるので性能アップの期待ができます(Linux)
    private lazy val transport = conf.getString("transport") match {
      case "native" => Native
      case "jdk" => Jdk
    }
  • PlayRequestHandler がアプリの処理(コントローラ)呼び出しになります。
  • つまり

🎊 起動処理おしまい 🎊


まとめ

  • ProdServerStart#main で開始
  • Configurationをロード
  • ApplicationLoader/GuiceApplicationLoader/GuiceApplicationBuilderでApplicationを作る
  • Play.start で Applicationを開始したら
  • NettyServerProviderでNettyServerを生成すればHTTPサーバが起動して準備完了!
  • PlayRequestHandler を読めばリクエストが届いてコントローラを呼び出すまでに入れます

補足

  • Typsafe Config/Google Guice/JBoss Netty/Akka/Akka-Stream の知識が多少ないとしんどい 😅
    • ついでに覚えれてラッキーという気持ちで ✨
  • sbt run だと実は別フローで起動する
    • ホットリロードもあって、ちょい難しいので、先にProdServerからやる方がオススメ

ここまでの反省

  • 入門なので優しく丁寧にと思ったら、サーバ起動するだけで、結構長くて、初学者の心を折りにいっただけだった気がする
  • スライドに絵文字つけたら優しい感じなるかと思ったけど、冷静に考えてそんなわけなかった。
    • 逆に多用しすぎてうざい感じに仕上がった
    • 絵文字に頼ってはいけない。
  • 時間が。。。:innocent: 大変申し訳ない :persevere:

リクエストを受け取って
レスポンスを返すまで


  • ChannelInboundHandlerAdapter は nettyのclass
  • Nettyがリクエストを受け取ったら、オーバーライドしたメソッドを呼び出してくれる
  • def channelRead 内で def handle を呼び出すのが主な仕事
private[play] class PlayRequestHandler(
  val server: NettyServer
) extends ChannelInboundHandlerAdapter {
  // ...
  override def channelRead(ctx: ChannelHandlerContext, msg: Object): Unit = { /* ... */ }
    // Handle the given request.
  def handle(
    channel: Channel, request: HttpRequest
  ): Future[HttpResponse] = { /* メインのリクエスト処理 */ }
}

  • Nettyから HttpRequestを受け取ってhandleメソッドを呼び出し、レスポンスをWriteする
  • trampoline は特殊なExecutionContextで面白い!
override def channelRead(ctx: ChannelHandlerContext, msg: Object): Unit = {
  logger.trace(s"channelRead: ctx = ${ctx}, msg = ${msg}")
  msg match {
    case req: HttpRequest =>
      requestsInFlight.incrementAndGet()
      /* handle メソッド呼び出すとレスポンスが返ってくる */
      val future: Future[HttpResponse] = handle(ctx.channel(), req)
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      lastResponseSent = lastResponseSent.flatMap { /* 省略 */ 
        ctx.writeAndFlush(httpResponse) /* レスポンスをWrite */
      }
  }
}

  • NettyModelConversion を使って Nettyの io.netty.handler.codec.http.HttpRequest をPlayの play.api.mvc.RequestHeader に変換(Try[RequestHeader])
def handle(
  channel: Channel, request: HttpRequest
): Future[HttpResponse] = {
  import play.api.libs.iteratee.Execution.Implicits.trampoline
  val requestId = RequestIdProvider.requestIDs.incrementAndGet()
  // ココでリクエストを変換
  val tryRequest = modelConversion.convertRequest(
    requestId,
    channel.remoteAddress().asInstanceOf[InetSocketAddress],
    Option(channel.pipeline().get(classOf[SslHandler])),
    request
  )
}

PlayRequestHandler.handle 続き

  val (requestHeader, resultOrHandler) = tryRequest match {
    case Failure(exception: TooLongFrameException) => clientError(Status.REQUEST_URI_TOO_LONG, exception.getMessage)
    case Failure(exception) => clientError(Status.BAD_REQUEST, exception.getMessage)
    case Success(untagged) =>
      server.getHandlerFor(untagged) match {
        case Left(directResult) =>
          untagged -> Left(directResult)
        case Right((taggedRequestHeader, handler, application)) =>
          taggedRequestHeader -> Right((handler, application))
      }
  }
  • Successの場合は、server.getHandlerFor を呼び出す
  • RightはApplicationに設定されたhandlerを返す
  • LeftはResultが返る

  • リクエストの Handler を探す
  • 見つかったらRightで Handler を返し、見つからなかったら、LeftでエラーのResultを返す
  • 例外的に WebCommandもLeftでResult返す
  • DefaultHttpRequestHandlerのhandleForRequestへ続く
def getHandlerFor(
  request: RequestHeader
): Either[Future[Result], (RequestHeader, Handler, Application)] = {
  /* 省略版 */  
  application.requestHandler.handlerForRequest(request) match {
    case (requestHeader, handler) => Right((requestHeader, handler, application))
  }
}

  • HEADが自動的にGETとしても動作
def handlerForRequest(request: RequestHeader) = {
  /* ... */
  val (routedRequest, handler) = routeRequest(request) map {
    case handler: RequestTaggingHandler =>
                         (handler.tagRequest(request), handler)
    case otherHandler => (request, otherHandler)
  } getOrElse {
    // We automatically permit HEAD requests against any GETs without the need to
    // add an explicit mapping in Routes
    request.method match {
      case HttpVerbs.HEAD =>
        routeRequest(request.copy(method = HttpVerbs.GET)) match {
          /* 省略 */
  /* 省略 */
  (routedRequest, filterHandler(rh => handler)(routedRequest))
}

class DefaultHttpRequestHandler {
  def routeRequest(request: RequestHeader): Option[Handler] = {
    router.handlerFor(request)
  }
}

trait Router {
  def handlerFor(request: RequestHeader): Option[Handler] = {
    routes.lift(request)
  }
}

class RoutesProvider { /* 省略版 */
  lazy val get = {
    val prefix = httpConfig.context
    val router = Router.load(environment, configuration)
      .fold[Router](Router.empty)(injector.instanceOf(_))
    router.withPrefix(prefix)
  }
}

routeRequest 補足
  • Router は BuiltinModule にて bind[Router].toProvider[RoutesProvider]
  • RoutesProvider#get にて、routes-compilerで自動生成されたクラスをロード
  • 自動生成された Routes の routes メソッドを呼び出す
  • サンプルをあげときました Gist

routes.Routes

  • conf/routes の内容から、routes-compiler が生成
  • target/scala-2.11/routes/main/router/Routes.scala
// こんな感じのScalaコードが生成されてますよ
class Routes(
  override val errorHandler: play.api.http.HttpErrorHandler, 
  // @LINE:6
  HomeController_0: controllers.HomeController,
  // @LINE:8
  CountController_3: controllers.CountController,
  // @LINE:10
  AsyncController_2: controllers.AsyncController,
  // @LINE:13
  Assets_1: controllers.Assets,
  val prefix: String
) extends GeneratedRouter

  • RequestHeader を受け取って Handler を返すPartialFunction
def routes: PartialFunction[RequestHeader, Handler] = {
  // @LINE:6
  case controllers_HomeController_index0_route(params) =>
    call { 
      controllers_HomeController_index0_invoker.call(
        HomeController_0.index
       )
    }
  // @LINE:8
  case controllers_CountController_count1_route(params) =>
    call { 
      controllers_CountController_count1_invoker.call(
        CountController_3.count
      )
    }
  /* 省略 */
}

Handler が決まりました! src

val (requestHeader, resultOrHandler) = /* さっきまでいたところ */

resultOrHandler match {
  //execute normal action
  case Right((action: EssentialAction, app)) =>
    val recovered = EssentialAction { rh =>
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      action(rh).recoverWith {
        case error => app.errorHandler.onServerError(rh, error)
      }
    }
    handleAction(recovered, requestHeader, request, Some(app))
 /* 省略 */    
}
  • EssentialAction が大事そうです
  • EssentialAction { rh => からの action(rh)

EssentialActionに入る前に基本のおさらい

  • app/controllers/HomeController.scala
package controllers
import javax.inject.{ Inject, Singleton }
import play.api.mvc.{ Controller, Action, AnyContent }
@Singleton
class HomeController @Inject() extends Controller {
  def index: Action[AnyContent] = Action { req =>
    Ok(views.html.index("Your new application is ready."))
  }
}
  • conf/routes
GET     /                  controllers.HomeController.index
GET     /count             controllers.CountController.count
GET     /message           controllers.AsyncController.message

Controller と Action

def index: Action[AnyContent] = Action { req =>
  Ok(views.html.index("Your new application is ready."))
}
  • Action { req => は object Action のapplyメソッド
  • trait Action[A] のインスタンスを返す
  • A は そのActionが受け取るリクエストボディの型
  • Action[JsValue] だとJSON
  • reqは Request[A]で、 Aはリクエストボディの型
  • ユーザコードは、Request を受け取って、Result を返す処理

Action と BodyParser

  • Action[A] のインスタンスは BodyParser[A] を持っている
  • BodyParser[A] は リクエストボディをパースして A のオブジェクトに変換する
  • リクエストってなんなのか?
  • RequestHeader -> RequestBody(Array[Byte]) -> Result
  • RequestBodyをAsynchornousなStreamにしたい!
  • ブロッキングなInputStreamではなく、Akka-streamを使う

Accumulator[ByteString, Result]

  • Actionの仕事は RequestHeader => Body => Result
  • これをAccumulatorを使うと以下で表せる
  • RequestHeader => Accumulator[ByteString, Result]
  • AccumulatorはAkka-stream(Source)とつなぐためのオブジェクト
  • ByteString は Array[Byte]みたいなもの(こいつはAkkaのクラス)
  • わかりづらいのは、Asyncのためだと思って諦めるべし(AsyncなFWですしね)

基本のまとめ

  • Action[A] を作るのが コントローラのメソッドのお仕事
  • Action[A] は リクエストを受け取って Result を返すのが仕事
  • RequestHeader => Accumulator[ByteString, Result] で表現できる
  • 今更ですが、ソース読む前にPlayのドキュメント読んでおくことをオススメします

  • EssentialActionはHandlerをMixInしている
  • Action は EssentialAction を継承している
  • EssentialActionは RequestHeader => Accumulator[ByteString, Result] を継承している ⁉️
trait EssentialAction
  extends (RequestHeader => Accumulator[ByteString, Result])
  with Handler { self =>
  def apply() = this
}

trait Action[A] extends EssentialAction

補足: Function も継承できる

  • だってFunctionもただの型だもの
  • trait Function1[-T1, +R]
  • RequestHeader => Accumulator[ByteString, Result]
  • Funtion1[RequestHeader, Accumulator[ByteString, Result]]
  • これ同じ
// こう書けるけど、Intellijさんに -T1 => R 形式で書けってオススメされる
trait EssentialAction
 extends Function1[RequestHeader,  Accumulator[ByteString, Result]]

  • BodyParser でリクエストボディをパース
  • ユーザコードの呼び出しがついにされる
  def apply(request: Request[A]): Future[Result] = {
    /* ユーザコード */
  }
  // 省略版なので注意
  def apply(rh: RequestHeader): Accumulator[ByteString, Result] =
    parser(rh).mapFuture { // parser はBodyParser
      case Left(r) => /* ... パース失敗 (T_T) */
      case Right(a) => /* パース成功! */
        val request = Request(rh, a)
        apply(request) // 上のapply呼び出し
    }(executionContext)

resultOrHandler match {
  //execute normal action
  case Right((action: EssentialAction, app)) =>
    val recovered = EssentialAction { rh =>
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      // action(rh) は Accumulatorを返す
      action(rh).recoverWith {
        case error => app.errorHandler.onServerError(rh, error)
      }
    }
    // イマココ
    handleAction(recovered, requestHeader, request, Some(app))
 /* 省略 */    
}

  • ついにActionが実行される
private def handleAction(action: EssentialAction, requestHeader: RequestHeader,
  request: HttpRequest, app: Option[Application]): Future[HttpResponse] = {
  for {
    bodyParser <- Future(action(requestHeader))(mat.executionContext)
    actionResult <- {
      val body = modelConversion.convertRequestBody(request)
      (body match {
        case None => bodyParser.run()
        case Some(source) => bodyParser.run(source)
      }).recoverWith { /* ... */ }
    }
    validatedResult <- { /* Clean and validate the action's result */ }
    convertedResult <- { /* Nettyの HttpResponseへ */
      modelConversion.convertResult(validatedResult, requestHeader, request.getProtocolVersion, errorHandler(app))
    }
  } yield convertedResult
}

handleAction の補足

  • bodyParserは Actionのapplyなので、Accumulator[ByteString, Result]が入る
  • body は Nettyのリクエストボディを取り出して、ボディがある場合は渡して、Accumulatorのrunを呼び出す
  • actionResult は Resultが入る。つまり、コントローラでOkとか返したものが入る
  • その Result を NettyのHttpResponse に変換して、完了!

ようやく終わりですよ src

override def channelRead(ctx: ChannelHandlerContext, msg: Object): Unit = {
  msg match {
    case req: HttpRequest =>
      requestsInFlight.incrementAndGet()
      val future: Future[HttpResponse] = handle(ctx.channel(), req)
      /* handle メソッド呼び出し完了 */
      /* ここまで読んだんですよ!!! */
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      lastResponseSent = lastResponseSent.flatMap { /* 省略 */ 
        ctx.writeAndFlush(httpResponse)
      }
  }
}
  • ついに戻ってきて HttpResponse をwriteAndFlushしたので、リクエストを受け取ってレスポンスを返すまで終わりました!

みなさま
本当に お疲れ様でした 😭


質問あればどうぞー!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment