Play: Архитектура Action'ов
Часть ядра фреймворка Play2, относящаяся собственно к обработке веб-запросов - сравнительно небольшая, и основным типом в нем является Action
, т.е. команда, и некоторое количество вспомогательных типов (Request
, Result
, BodyParser
и др.).
В самом грубом приближении, ядро Play2 представляет собой API, которое занимается преобразованием вида:
RequestHeader -> Array[Byte] -> Result
Приведенное вычисление принимает на вход заголовок RequestHeader
, затем принимает тело запроса как Array[Byte]
и генерирует Result
.
Этот тип предполагает вычитку всего тела запроса в память или на диск, а это не всегда хорошо с точки зрения расходования памяти.
В этом случае мы хотели бы получать тело запроса в виде блоков и обрабатывать их по мере поступления, если это необходимо.
Т.е. неплохо было бы поменять вторую стрелочку таким образом, она принимала на вход вот такие блоки и в конечном счете генерировала бы результат. И необходимый нам тип действительно существует, называется он Iteratee
, и параметризуется двумя типами - тип входного параметра и тип результата.
Т.о., Iteratee[E,R]
принимает на вход тип E
и возвращает тип R
, конкретно в данном случае - принимающий на вход Array[Byte]
, возвращающий Result
. Т.е. мы немного меняем тип вот так:
RequestHeader -> Iteratee[Array[Byte],Result]
Первую стрелочку мы просто заменяем на Function[From,To]
, т.е. фактически заменяем стрелочку на символ =>
:
`
RequestHeader => Iteratee[Array[Byte],Result]
Как мы знаем, для более выразительного построения новых типов с использованием уже существующих мы можем использовать инфиксные операторы типов. Если мы объявим псевдоним типа Iteratee[E,R]
type ==>[E,R] = Iteratee[E,R]
То теперь мы можем использовать его в качестве инфиксного оператора типа:
RequestHeader => Array[Byte] ==> Result
Что означает следующее: на вход принимаем заголовки запроса, на вход же принимаем тело запроса в виде Array[Byte]
и в финале возвращаем Result
. Приблизительно таким образом объявлен трейт EssentialAction
, который является базовым для всех команд (action
ов) (инфиксный оператора типов там не используется, мы привели его просто для наглядности):
trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])
В то же время можно сказать, что тип Result
является абстрактным представлением заголовков и тела ответа. В первом приближении такой тип выглядел бы вот так:
case class Result(headers: ResponseHeader, body:Array[Byte])
Но, опять же, как и в ситуации с запросом, мы бы хотели не сразу сформировать весь массив байт ответа (потому что он может быть довольно большим и занять всю память), а постепенно, блоками отдавать его клиенту. Поэтому нам неплохо было бы заменить Array[Byte]
чем-то вроде генератора блоков байт.
Для этого у нас уже есть необходимый тип - Enumerator[E]
, который может генерировать блоки типа E
, в нашем случае - Enumerator[Array[Byte]]
:
case class Result(headers:ResponseHeaders, body:Enumerator[Array[Byte]])
Если же нам все-таки не нужно отсылать ответ постепенно, а мы хотим отдать все тело ответа сразу, мы можем отослать все данные в одном блоке.
Любой тип данных E
, который можно сконвертировать в поток байт, или Array[Byte]
, может быть потенциально отдан в виде потока - за это отвечает объект типа Writeable[E]
, который отдается в виде implicit’ного объекта, несколько упрощенно это можно представить как:
case class Result[E](headers:ResponseHeaders, body:Enumerator[E])(implicit writeable:Writeable[E])
На самом деле, правда, writeable
передается не в конструкторе Result
а, а, например, в методе-фабрика в классе Status
, который тоже является Result
ом.
def apply[C](content: C)(implicit writeable: Writeable[C]): Result
EssentialAction
- всего лишь трейт, то есть интерфейс. В контроллерах реально будет использоваться производный от него Action
, а точнее Action[A]
. Action[A]
наследуется от EssentialAction
, при этом он уже может преобразовать типизированный Request[A]
, а не просто поток байт, но для типа A
должен быть предоставлен BodyParser[A]
.
trait Action[A] extends EssentialAction {
type BODY_CONTENT = A
def parser: BodyParser[A]
def apply(request: Request[A]): Future[Result]
//...
}
В силу асинхронной природы Iteratee
, результатом Action
‘а уже является Future[Result]
вместо Result
. Action
, вообще говоря тоже трейт, при реализации которого надо переопределить методы apply
и parser
.
Итак, мы установили, что (в подавляющем большинстве) запросы приложения, написанного с использованием Play, обрабатываются с помощью Action
, который есть по сути функция (play.api.mvc.Request => play.api.mvc.Result)
. Приведем пример простейшего Action
а, т.е. “команды”
val echo = Action { request =>
Ok("Got request [" + request + "]")
}
Тело Action
‘а возвращает значение play.api.mvc.Result
, представляющее HTTP ответ, отсылаемый клиенту. В данном случае Ok
вернет ответ со статусом 200 OK
, и типом тела ответа text/plain
. Собственно, Ok
- это константа
val Ok = new Status(OK)
Как мы уже говорили, фактически Status
представляет собой вариант Result
а:
class Status(status: Int) extends Result { // ...
def apply[C](content: C)(implicit writeable: Writeable[C]): Result = ...
...
}
Вообще, надо заметить, что т.н. companion-object Action
наследует ActionBuilder[Request]
, в котором есть ряд методов-фабрик; в данном случае вызывается метод:
final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.default)(block)
Т.е. используется BodyParsers.parse.default
, который парсит тело запроса исходя из содержимого заголовка "Content-Type"
.
Вообще говоря, BodyParser[A]
является на самом деле Iteratee[Array[Byte],A]
, т.е. почти то же самое, что мы видели в EssentialAction
, за исключением того, что вместо Result
здесь A
.
Методы для создания разных видов команд (Action
ов), как мы уже говорили, находятся в трейте ActionBuilder
. Очевидно, что мы можем реализовать свой ActionBuilder
, и использовать его для создания нужных нам видов команд. Например, пусть нам надо создать декоратор для логирования, т.е. фактически каждый вызов команды, созданной нами, будет логироваться.
Мы можем реализовать эту функциональность в методе invokeBlock
, который вызывается для каждой команды, созданной ActionBuilder
ом:
import play.api.mvc._
object LoggingAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
Logger.info("Calling action")
block(request)
}
}
И теперь создадим логируемую команду:
def index = LoggingAction {
Ok("Hello World")
}
Композиция команд
Иногда возникает необходимость в нескольких видах ActionBuilder
‘ов, например, если у нас разные виды аутентификации. Но в то же время мы не хотели бы отказываться от нашей логируемой команды, т.е. у нас возникает необходимость скомбинировать несколько ActionBuilder
‘ов.
Сделать это можно, например, вложением одной команды в другую, т.е. путем передачи некоторой команды action
в наш логируемый вариант:
import play.api.mvc._
case class Logging[A](action: Action[A]) extends Action[A] {
def apply(request: Request[A]): Future[Result] = {
Logger.info("Calling action")
action(request)
}
lazy val parser = action.parser
}
В принципе, то же самое можно сделать и без определения отдельного класса:
import play.api.mvc._
def logging[A](action: Action[A])= Action.async(action.parser) { request =>
Logger.info("Calling action")
action(request)
}
Есть еще в ActionBuilder
такая вещь, как метод composeAction
, специально созданный для этих целей:
object LoggingAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
block(request)
}
override def composeAction[A](action: Action[A]) = new Logging(action)
}
После чего мы опять-таки можем просто вызвать LoggingAction:
def index = LoggingAction {
Ok("Hello World")
}
Ну и конечно, подмешивание команд можно делать и без создания отдельного ActionBuilder
а, просто вложением команд (правда, в этом случае мы теряем преимущества повторного использования, т.е. этот вариант годится, если, например, нам нужно залогировать только одну команду):
def index = Logging {
Action {
Ok("Hello World")
}
}