- ☕ 前段
- 🍅 ソースコードツリーを眺めよう!
- 🍖 起動処理を追いかけよう!
- 🍰 リクエストを受け取ってレスポンスを返すまで
残念ながら時間不足の予感です。3と4で区切りますので、お時間ある方のみ 4 までどうぞ。 3までは予定通り 20:00で完了予定です。
- 田中智文 タナカトモフミ @tanacasino
- 高知県高知市百石町から上京中
- 二児の父 👶 👶
- 株式会社ビズリーチ ターミナル部 部長
- それゆけ!ターミナル部 連載中!
- Scalaを業務で2年ほど。その前はEucalytpus、OpenStack、スクラムなどなど
- GitBucket の幽霊部員(Committer)
- スライドでナビしますので、手元でソースコードを開いて読んでみてスタイル
- 随時気になる点はアピってください。顔で進捗を表現していただければペースを調整できる可能性があります 😉
- スライドのソースは長さの問題で省略版なので、手元のソースコードを正としてみてください
- スライド見づらい会場っぽいので、アップロードしました。ダウンロードして手元でもご覧下さい
- スライドにGitHubのソースコードへのリンク作ってますので、迷子になったらクリック
- documentation
- 公式ドキュメントのソース
- framework
- フレームワークの本体ソースコード
- 今日のメイン
- templates
- Playのプロジェクトの雛形
- Lightbend社のHPからダウンロードできる雛形のソースコード play-scala など
- framework/src
- 大量のプロジェクトを発見
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
- 適当にえいや!で決める
- ActionとかControllerとか使うとすぐに出てるところから読む
- Jsonとか簡単そうなところから始める
- Applicativeとかモナモナしてるところから始める
def main(args: Array[String]): Unit
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
を実行してるだけ!!!
- framework/src/play-server/src/main/scala/play/core/server/ProdServerStart.scala
- たったの2行 😆
- 読める気がしてきました 👍
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)
// ... 省略
}
- コンフィグの読み込み
- PID(Process ID) ファイルを作成する
play.api.Application
を作って初期化する- 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")
// ... 省略
- SystemProperty (System.getProperty)
- DirectConfig: コマンドライン引数
- アプリの設定(conf/application.confなど)
- play overrides config
- Akka等の外部ライブラリの設定をPlayが推奨する値で上書き
- 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は独自のクラスに置き換えできる
- Playでは Runtime DIとCompile time DIをサポートしている
play.application.loader
にApplicationLoader
を継承したクラスを設定することで、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]
の正体に辿りついたけど。。。
- Guice Injectorを生成する際の、configから読み込むモジュールに実はある
- framework/src/play/src/main/resources/reference.conf
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 + ")")
}
}
- 過去の互換性もあり怪しい感じですが、見たことある起動メッセージの表示まで来ました 🎉
ApplicationLoader
を使ってApplication
を作るApplicationLoader
は差し替え可能- デフォルトではGuiceを使うので以下を覚えておけば良さそう
GuiceApplicationLoader
GuiceApplicationBuilder
BuiltinModules
DefaultApplication
- そうです。これから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.server.provider
で設定されているクラスをnewしてるだけ- デフォルト設定はココ framework/src/play-netty-server/src/main/resources/reference.conf
- 正体は
play.core.server.NettyServerProvider
- akka-httpに切り替え可(まだexperimental)
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)
/* ... */
}
}
- akka-stream と netty の世界とつないでます (Source/Sink)
play.server.netty.transport
にnative
を指定すると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)
}
}
- Router は BuiltinModule にて
bind[Router].toProvider[RoutesProvider]
- RoutesProvider#get にて、routes-compilerで自動生成されたクラスをロード
- 自動生成された Routes の routes メソッドを呼び出す
- サンプルをあげときました Gist
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)
- 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
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[A]
のインスタンスはBodyParser[A]
を持っているBodyParser[A]
は リクエストボディをパースして A のオブジェクトに変換する- リクエストってなんなのか?
- RequestHeader -> RequestBody(Array[Byte]) -> Result
- RequestBodyをAsynchornousなStreamにしたい!
- ブロッキングなInputStreamではなく、Akka-streamを使う
- 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もただの型だもの
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
}
- 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したので、リクエストを受け取ってレスポンスを返すまで終わりました!