Статьи

Понимание API Play Filter

С Play 2.1, горячим от прессы, было много людей, спрашивающих о новом API фильтра Play. На самом деле API невероятно прост:

1
2
3
trait EssentialFilter {
  def apply(next: EssentialAction): EssentialAction
}

По сути, фильтр — это просто функция, которая выполняет действие и возвращает другое действие. Обычная вещь, которую должен сделать фильтр — это обернуть действие, вызвав его как делегат. Чтобы затем добавить фильтр в ваше приложение, вы просто добавляете его в метод Global doFilter . Для этого мы предоставляем вспомогательный класс:

1
2
3
object Global extends WithFilters(MyFilter) {
  ...
}

Легко ли? Заверните действие, зарегистрируйте его в глобальном. Ну, это легко, но только если вы понимаете архитектуру Plays. Это очень важно, потому что как только вы поймете архитектуру Play, вы сможете сделать гораздо больше с Play. У нас есть некоторая документация, которая объясняет архитектуру Plays на высоком уровне. В этом посте я собираюсь объяснить архитектуру Play в контексте фильтров, фрагментов кода и вариантов использования.

Краткое введение в архитектуру Plays

Мне не нужно вдаваться в подробности, потому что я уже предоставил ссылку на нашу документацию по архитектуре, но вкратце архитектура Play очень хорошо соответствует потоку HTTP-запросов. Первое, что приходит, когда выполняется HTTP-запрос, — это заголовок запроса. Поэтому действие в Play должно быть функцией, принимающей заголовок запроса. Что происходит дальше в HTTP-запросе? Тело получено. Итак, функция, которая получает запрос, должна возвращать то, что потребляет тело. Это итератор, который является обработчиком реактивного потока, который в конечном итоге дает один результат после использования потока. Вам необязательно понимать детали того, как работают итераторы, чтобы понимать фильтры. Важно понимать, что итераторы в конечном итоге выдают результат, который вы можете отобразить, как и будущее, используя свою функцию map . Подробнее о написании повторяющихся читателей читайте в моем блоге . Следующее, что происходит в HTTP-запросе, это то, что HTTP-ответ должен быть отправлен. Так каков результат итерации? HTTP-ответ. А HTTP-ответ — это набор заголовков ответа, за которым следует тело ответа. Тело ответа — это перечислитель, который является источником реактивного потока. Все это отражено в черте Plays EssentialAction :

1
trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])

Это означает, что основным действием является функция, которая принимает заголовок запроса и возвращает итератора, который потребляет куски тела байтового массива и в конечном итоге выдает результат.

Более простой способ

Прежде чем продолжить, я хотел бы отметить, что Play предоставляет вспомогательную черту под названием Filter которая упрощает написание фильтров, чем при использовании EssentialFilter . Это похоже на черту Action , в которой Action упрощает написание EssentialAction поскольку не нужно беспокоиться об итераторах и о том, как анализируется тело. Вместо этого вы просто предоставляете функцию, которая принимает запрос с проанализированным телом, и возвращает результат. Черта Filter упрощает вещи подобным образом, однако я собираюсь оставить это обсуждение до конца, потому что я думаю, что лучше понять, как фильтры работают снизу вверх, прежде чем вы начнете использовать вспомогательный класс.

Нуп фильтр

Чтобы продемонстрировать, как выглядит фильтр, первое, что я покажу, это noop-фильтр:

1
2
3
4
5
6
7
class NoopFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
      next(request)
    }
  }
}

Каждый раз, когда выполняется фильтр, мы создаем новое действие EssentialAction которое оборачивает его. Поскольку EssentialAction — это просто функция, мы можем просто вызвать ее, передав переданный запрос. Таким образом, вышесказанное является нашим основным шаблоном для реализации EssentialFilter .

Обработка заголовка запроса

Допустим, мы хотим посмотреть заголовок запроса и условно вызвать завернутое действие на основе того, что мы проверяем. Примером фильтра, который может это сделать, может быть общая политика безопасности для области /admin вашего сайта. Это может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
class AdminFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
      if (request.path.startsWith('/admin') && request.session.get('user').isEmpty) {
        Iteratee.ignore[Array[Byte]].map(_ => Results.Forbidden())
      } else {
        next(request)
      }
    }
  }
}

Здесь вы можете видеть, что, поскольку мы перехватываем действие до того, как тело было проанализировано, нам все еще нужно предоставить анализатор тела, когда мы блокируем действие. В этом случае мы возвращаем синтаксический анализатор тела, который просто игнорирует все тело, и сопоставляем его с результатом запрещения.

Обращаться с телом

В некоторых случаях вы можете захотеть что-то сделать с телом в вашем фильтре. В некоторых случаях вы можете захотеть разобрать тело. Если это так, рассмотрите возможность использования композиции действия вместо этого, поскольку это позволяет подключиться к обработке действия после того, как действие проанализировало тело. Если вы хотите проанализировать тело на уровне фильтра, то вам придется его буферизовать, проанализировать, а затем снова передать в поток для повторного анализа действия. Однако есть некоторые вещи, которые можно легко сделать на уровне фильтра. Одним из примеров является распаковка gzip. Play Framework уже обеспечивает распаковку gzip из коробки, но если это не так, то это может выглядеть так (используя перечисление gunzip из моего проекта play extra iteratees ):

01
02
03
04
05
06
07
08
09
10
11
class GunzipFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
      if (request.headers.get('Content-Encoding').exists(_ == 'gzip')) {
        Gzip.gunzip() &>> next(request)
      } else {
        next(request)
      }
    }
  }
}

Здесь, используя состав итерируемого, мы оборачиваем итератор парсера тела в перечисляемый список.

Обработка заголовков ответа

При фильтрации вы часто захотите что-то сделать с отправляемым ответом. Если вы просто хотите добавить заголовок, добавить что-то в сеанс или выполнить какую-либо операцию записи в ответе, фактически не читая его, то это довольно просто. Например, предположим, что вы хотите добавить собственный заголовок к каждому ответу:

1
2
3
4
5
6
7
8
class SosFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
      next(request).map(result =>
        result.withHeaders('X-Sos-Message' -> 'I'm trapped inside Play Framework please send help'))
    }
  }
}

Используя функцию map для итератора, который обрабатывает тело, нам предоставляется доступ к результату, полученному действием, который мы можем затем изменить, как показано. Однако, если вы хотите прочитать результат, вам нужно будет развернуть его. Результаты воспроизведения являются либо AsyncResult либо PlainResult . AsyncResult — это Result который содержит Future[Result] . У него есть метод transform который позволяет transform возможный PlainResult . У PlainResult есть заголовок и тело. Допустим, вы хотите добавить временную метку к каждому вновь созданному сеансу, чтобы записывать, когда он был создан. Это можно сделать так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class SessionTimestampFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
 
      def addTimestamp(result: PlainResult): Result = {
        val session = Session.decodeFromCookie(Cookies(result.header.headers.get(HeaderNames.COOKIE)).get(Session.COOKIE_NAME))
        if (!session.isEmpty) {
          result.withSession(session + ('timestamp' -> System.currentTimeMillis.toString))
        } else {
          result
        }
      }
 
      next(request).map {
        case plain: PlainResult => addTimestamp(plain)
        case async: AsyncResult => async.transform(addTimestamp)
      }
    }
  }
}

Обработка тела ответа

Последнее, что вы можете сделать, — это преобразовать тело ответа. PlainResult имеет две реализации: SimpleResult для тел без кодирования передачи и ChunkedResult для тел с кодированием передачи. SimpleResult содержит перечислитель, а ChunkedResult содержит функцию, которая принимает итератора для записи результата. Примером того, что вы можете захотеть сделать, является реализация фильтра gzip. Очень наивная реализация (например, не используйте это, вместо этого используйте мою полную реализацию из моего проекта play extra iteratees ) может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class GzipFilter extends EssentialFilter {
  def apply(next: EssentialAction) = new EssentialAction {
    def apply(request: RequestHeader) = {
 
      def gzipResult(result: PlainResult): Result = result match {
        case simple @ SimpleResult(header, content) => SimpleResult(header.copy(
          headers = (header.headers - 'Content-Length') + ('Content-Encoding' -> 'gzip')
        ), content &> Enumeratee.map(a => simple.writeable.transform(a)) &> Gzip.gzip())
      }
 
      next(request).map {
        case plain: PlainResult => gzipResult(plain)
        case async: AsyncResult => async.transform(gzipResult)
      }
    }
  }
}

Использование более простого API

Теперь вы узнали, как добиться всего, используя базовый API EssentialFilter , и, надеюсь, поэтому вы понимаете, как фильтры вписываются в архитектуру Play и как вы можете использовать их для достижения своих требований. Давайте теперь посмотрим на более простой API:

01
02
03
04
05
06
07
08
09
10
11
12
trait Filter extends EssentialFilter {
  def apply(f: RequestHeader => Result)(rh: RequestHeader): Result
  def apply(next: EssentialAction): EssentialAction = {
    ...
  }
}
 
object Filter {
  def apply(filter: (RequestHeader => Result, RequestHeader) => Result): Filter = new Filter {
    def apply(f: RequestHeader => Result)(rh: RequestHeader): Result = filter(f,rh)
  }
}

Проще говоря, этот API позволяет вам писать фильтры, не беспокоясь о парсерах тела. Похоже, что действия — это просто функции заголовков запросов к результатам. Это ограничивает всю мощь того, что вы можете делать с фильтрами, но для многих случаев использования вам просто не нужна эта мощь, поэтому использование этого API предоставляет простую альтернативу. Чтобы продемонстрировать, класс фильтра noop выглядит так:

1
2
3
4
5
class NoopFilter extends Filter {
  def apply(f: (RequestHeader) => Result)(rh: RequestHeader) = {
    f(rh)
  }
}

Или, используя сопутствующий объект Filter :

1
2
3
val noopFilter = Filter { (next, req) =>
  next(req)
}

И фильтр синхронизации запроса может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
val timingFilter = Filter { (next, req) =>
  val start = System.currentTimeMillis
 
  def logTime(result: PlainResult): Result = {
    Logger.info('Request took ' + (System.currentTimeMillis - start))
    result
  }
 
  next(req) match {
    case plain: PlainResult => logTime(plain)
    case async: AsyncResult => async.transform(logTime)
  }
}

Ссылка: Понимание API-интерфейса Play Filter от нашего партнера по JCG Джеймса Ропера в блоге Джеймса и Бет Роперов .