General philosophy
Since you're reading this, you're probably already familiar with the framework, but here is a reminder of the things you absolutely need to know:
Play is stateless. As far as the framework is concerned, nothing gets stored on the server between requests. Of course a typical web app has some sort of persistence engine (SQL, NoSQL, files etc.), but that's not part of the framework. Stateless is opposed to stateful (thank you captain obvious!), where the framework keeps data about the user between request. Play has something called a session, but it's actually just a cookie, which explains why you can only store Strings in there.
Play is reactive. Behind the buzzword, it means play will use as few threads as possible, and therefore, threads are "shared" between clients. That's very important for long lived connections like Server Sent Events, or Websockets. A typical JEE application is giving a thread to each HTTP connection (at least until JSR 315: Servlet 3.0), play does not. It has HUGE impacts on both the framework design and the way you're supposed to use it (especially the things you're NOT supposed to do).
Version 2 is a complete rewrite, and a completely different beast. The (java) user API is meant to feel familiar to a play 1 user thought.
Much more focus is put on type safety, which should not come as a surprise since play is backed by a company called Typesafe.
The internals are designed in a mix of functional programming and object orientation. Most of it is written in Scala, and the framework has a Java "translation" layer on top of it. Generally, the code is fairly simple, and you don't need to be a Scala expert to understand what's going on. It avoids mutable states as much as possible (Which is preferred in Scala).
It's open source! Hopefully you'll find everything you need to know to contribute here. The community is waiting for your pull requests. Contributions to the docs or bugfixes are always welcome.
Let's get started!
Launching a play app
So, you typed play start
, what happened ?
Well actually, You've just called sbt, the default Scala Build Tool (yes, 'S' stands for Scala not for Simple) with a bunch of specific parameters.
Sbt "reads" your application build definition, and finds out that it's using a plugin called play % sbt-plugin
.
You can see it in project/plugins.sbt
( here in the HelloWorld sample).
This plugin has a config key telling sbt where the entry point (the main) of the application is. I won't cover sbt here (play build alone could use a pretty big article), but it says here that main lives in play.core.server.NettyServer
In play, and contrary to standard JEE, there's no "application server" hosting applications. The application you've created using play new
just is the server, and Play is just one of it's dependencies (precisely, several dependencies since play is split in modules).
As you guessed, Play is simply currently based on the very famous (and java based) Netty. Sbt will look into this object to find a main. You probably noticed a method called mainDev
, obviously this one is called in dev mode (when you use play run
). We'll just focus on main
for now.
As you can tell reading the code, it just parses the arguments, creates a file containing the PID (in case you want to stop the app at some point), and does some other boring initialization boilerplate. Let's skip it to the interesting bits:
Ah! There it is. Eventually a new NettyServer
is created with a bunch of arguments. But first, since scala is not lazy, it's evaluates new StaticApplication(applicationPath)
.
Bootstrap
First thing first, a Play application needs to be initialized. Let's have a look at that StaticApplication
. "Why is it called a Static application?" you may ask. Simply because it won't hot redeploy code changes (We're in prod mode remember? Sensible people don't hot redeploy in production).
So we get there. We create a DefaultApplication
, which is basically just a case class containing general info about the current app, like app folder, configuration, classloader, etc. (code here), and we call Play.start(application)
.
What it's going to do is fairly simple:
Just call onStart
on each plugin, one by one, making sure they're using the correct classloader.
Those plugins will, for example, create a connection pools to a DB.
At that point, if a plugin throws an exception, the application will just be stopped.
So we know the application configuration, we have started our plugins successfully. We're now ready to start listening for HTTP requests.
Listening for Http Requests.
It's now time to create a NettyServer
. As we've seen, we create an instance of the NettyServer
class (not to be confused with the NettyServer
object).
This class will create an instance of Server
, configuring it's threads pools, the pipeline encoder and decoder, bind the server to an adress and port, configure SSL, but more importantly this: newPipeline.addLast("handler", defaultUpStreamHandler)
which is:
So each time an HTTP or HTTPS request is received, Netty will call messageReceived
of this instance of PlayDefaultUpstreamHandler
.
This method does a few things:
- It create play a
play.api.mvc.RequestHeader
from Netty's HttpRequest. Basically filling it with headers values, parsing the query string, etc. But it does NOT try to parse the request body (not yet). - It manages the flash cookie, making sure it only lives for 1 request.
- It can "tag" the request, adding metadata in the request object about routing.
- It uses the application
Global
object to find out what to do with this request:
What it does here is handling exceptions and parameters parsing. If something fail,
onBadRequest(rh, e.getMessage)
is called on the application Global
object (if no global is provided, it's using the default Global) which should render an Error page with status 500. Otherwise, it calls server.getHandlerFor(rh)
which return Either[Result, (Handler, Application)]
.
getHandlerFor
is there to resolve the Handler
to be called.
The two main types of Handlers are:
- EssentialAction: Basically that's what you defined in your Controllers by writing
Action { request => ... }
- WebSocket
All this code does is finding the Handler
to be invoked by calling Global.onRouteRequest
OR it returns the appropriate Response if an error occurred. If something failed, it will return a Left
containing a Response (for example a HTML page with status 500), otherwise it returns a Right
containing the Handler defined in your app (for example, an Action defined in a controller).
Dealing with the request body
Alright. So far we've somehow discovered the code that needs to be called. You may have noticed that we are only working with a RequestHeader
, that's a request without a body. Using pattern matching, we'll decide what to do with the value returned by getHandlerFor
.
The most common case by far is EssentialAction
:
Fundamentally, an EssentialAction
is just a function (RequestHeader) => Iteratee[Array[Byte], Result]
.
You give that function a RequestHeader
and it gives you an Iteratee
that consumes the request body reactively, and eventually "returns" a Result
.
The next step is now pretty obvious, since we have something to consume the request body, all we need to do is to feed it with Bytes, and we get our Result. That's exactly what handleAction
does. It will get bytes from the client, deal with chunking if necessary, and feed the Iteratee
to eventually get a Result
, but first it calls the Filters defined in your Global
. (val filteredAction = app.map(_.global).getOrElse(DefaultGlobal).doFilter(action)
).
In order to feed the Iteratee
, you need to create an Enumerator[Array[Bytes]]
:
Play creates an Enumerator
enumerating the chunks from the client, and compose it with a BodyParser
.
Then you just need to run the resulting Iteratee
to get a Future[Result]
, and to send this Result
back to the client.
And that's it!
TL;DR
play
is actually just an alias for sbt (+ funky options)- Each application is a server.
- Starting up an app means:
- Reading params + config
- Calling
onStart
on each plugin - Creating a Netty server and listening for HTTP requests
When a HTTP request gets into Play:
- The server calls
Global.onRouteRequest(rh: RequestHeader): Handler
- Most of the Time this
Handler
is actually anEssentialAction
- An
EssentialAction
is just a function(RequestHeader) => Iteratee[Array[Byte], Result]
. - This functions is called, and the resulting
Iteratee
is feed with chunks (Array[Byte]) from the request body. - Eventually, you get a
Result
, which is sent back to the client.
Next time, I'll write about DEV mode, hot reloading, and the relationship between Play and SBT.
Stay tuned!