Play! Lift! Srsly?

в 7:06, , рубрики: Без рубрики

Play! Lift! Srsly?Play! и Lift, — эти два фреймворка являются олицетворением того, куда движется основной поток Scala веб-разработчиков. Воистину, попробуйте поискать на Stack Overflow фреймворки для Scala и вы поймете что я прав. Я верю, что процент здравомыслящих людей, которым надоели сложные комбайны, велик, поэтому расскажу про «другой» фреймворк Xitrum.

Xitrum совершенно противоположен им по философии, это — минималистичный фреймворк, целью которого является непосредственно отдача контента. В нем нет магии и ни какого программирования по соглашению. Своим минимализмом он близок к Scalatra, но в отличие от него полностью асинхронен, т.к. построен на основе Netty (v4) и Akka (вот уже более года слежу за Scalatra и до сих пор поддержка Netty не заявлена). Но не пугайтесь, порог вхождения экстремально низок — акторы лишь опциональны, хотя и являются весомым плюсом в пользу фреймворка.

Сразу о производительности. В минимальной конфигурации xitrum запускается и работает с ограничением по памяти в 64Mb. Расходы по процессорному времени не значительны, т.е. сам фреймворк нагрузку на процессор не дает. Все остальное зависит от вас.

Отзывы пользователей
С официального сайта:

Wow, this is a really impressive body of work, arguably the most complete Scala framework outside of Lift (but much easier to use).

Xitrum is truly a full stack web framework, all the bases are covered, including wtf-am-I-on-the-moon extras like ETags, static file cache identifiers & auto-gzip compression. Tack on built-in JSON converter, before/around/after interceptors, request/session/cookie/flash scopes, integrated validation (server & client-side, nice), built-in cache layer (Hazelcast), i18n a la GNU gettext, Netty (with Nginx, hello blazing fast), etc. and you have, wow.

Мое мнение:

Лучший фреймворк который я когда-либо видел для Scala/Java. Xitrum меня действительно цепляет, это как смесь Dancer+Rails со статической типизацией, восхитительно!

Проект родом из Японии и имеет обстоятельную документацию. Разработчики проекта оставляют очень приятное впечатление, всегда прислушиваются к тому, что пишут в официальной группе и очень быстро закрывают баги. Вот уже почти год как я не получил ни одного отказа или зависшей задачи в трекере.

Ngoc Dao о своем проекте (из переписки)

Я начал разрабатывать Xitrum летом 2010 года, для использования в реальных проектах компании Mobilus. В то время, Play поддерживал только Java, а Lift был единственным полноценным фреймворком для Scala. Мы пытались его использовать несколько месяцев, но оказалось, что он не так прост, по крайней мере для нас знакомых с разработкой на Rails. Поэтому, как технический руководитель, я принял решение создать быстрый и масштабируемый веб-фреймворк на Scala для моей команды, настолько же простой в использовании, как и Rails. На самом деле, результат оказался больше похоже на Merb, нежели чем на Rails (в xitrum отсутствует слой доступа к данным).

С течением времени многие люди поучаствовали в разработки фреймворка. На данный момент команда, разрабатывающая ядро Xitrum состоит из двух человек: Oshida и Ngoc.

Итак, xitrum:

  • Типо безопасный (typesafe) во всех отношениях где это возможно
  • Полностью асинхронный. Необязательно слать ответ на запрос немедленно, можно запустить сложные вычисления и дать ответ, когда он будет готов. Очень легко реализуются такие штуки как Long polling, chunked response, WebSockets, SockJs, EventStream
  • Очень производительный, отдача статики сравнима по производительности с Nginx
  • Автоматическая сборка маршрутов (routes) приложения, нет нужды заводить какие-либо xml и прочее
  • Простая обработка параметров запроса, сессии и куки
  • Пре и пост фильтры
  • Встроенная поддержка кэширования ответов (в стиле Rails), поддержка ETag
  • Прекрасно подходит для разработки RESTful API, встроенная поддержка документирования на основе Swagger Doc
  • I18N на основе GNU gettext с динамической перезагрузки файлов перевода в случае их изменения. Автоматический генератор pot файлов из исходников
  • Модульность — xitrum автоматически объединяет маршруты из всех jar зависимостей
  • Подключаемый по требованию типо безопасный шаблонизатор Scalate или любой другой по вашему желанию

Xitrum является controller-first фреймворком. Очень легко динамически менять представления контроллера во время выполнения, что является не тривиальным для некоторых Scala/Java фреймворков. На моей памяти это вообще единственный фреймворк из мира Java который позволил без каких либо костылей написать CMS с динамической шаблонизацией, so sad.

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

Создание пустого проекта и структура папок

Новый проект проще всего создать так:

git clone https://github.com/ngocdaothanh/xitrum-new my-app
cd my-app
sbt/sbt run

По умолчанию сервер запустится на порту 8000. В проекте по умолчанию подключен шаблонизатор Scalate. Это идеальный проект для старта, в нем нет ничего лишнего, кроме стандартного контроллера и пары представлений которые можно удалить.

Что бы импортировать проект в eclipse используем sbt/sbt eclipse, в idea sbt/sbt gen-idea.
Важно: в eclipse нужно руками добавить папку config в classpath, иначе проект не будет запускаться из eclipse (баг sbt-eclipse#182).

Структура директории проекта:

./script		# скрипты используемые при разворачивании в production
./config		# папка конфигурации (akka, logback, xitrum)
./public		# папка со статикой (css, js, прочее)
./project		# sbt
./src			# src
./src/main/scalate	# папка с шаблонами 
./src/main/scala	# scala код
./src/main/scala/quickstart/Boot.scala  # точка входа в приложение

Простой контроллер

В xitrum каждый запрос может быть обработан только наследником от Action. Т. е. на каждый самостоятельный маршрут обрабатываемый нашим сервером мы должны объявить отдельный класс контроллер.

import xitrum.Action
import xitrum.annotation.GET

@GET("url/to/HelloAction")
class HelloAction extends Action {

  def execute() {
    respondHtml(
      <xml:group>
        <p>Hello world!</p>
      </xml:group>
    )
  }

}

Каждый новый запрос поступающий на сервер будет обрабатываться новым экземпляром класса, т. е. хранить состояние в этих классах не имеет смысла. Очень важно понять тот факт, что обработка запросов выполняется асинхронно. Пока вы не вызовете метод respond*(), соединение с клиентом не будет закрыто и клиент будет ждать вашего ответа, возможно вечность. Метод execute выполняется на Netty потоке, поэтому не следует помещать в него длительные операции, например:

@GET("url/to/HelloAction")
class HelloAction extends Action {

  def execute() {
    Thread.sleep(1000)  // ОШИБКА: блокирующая операция в Netty потоке
    respond()
  }

}

При такой реализации контроллера ваш сервер вряд ли сможет обслужить более 1 подключения в секунду. Что бы решить эту проблему нужно использовать либо FutureAction, либо ActorAction.

  • Action — метод exectue будет выполнен непосредственно в потоке Netty
  • FutureAction — метод execute будет выполнен в отдельном потоке (Akka system dispatcher)
  • ActorAction — в роли контроллера выступает обычный актор

Маршрутизация

Xitrum поддерживает все виды HTTP запросов с помощью аннотаций GET, POST и прочих. Любой контроллер может обрабатывать не ограниченно количество маршрутов. Можно определить порядок контроллеров с помощью аннотаций First и Last. Контроллер по умолчанию определяется как METHOD(":*")

@GET("url1")
@First
class A extends Action { ... }

@GET("url1", "url2", "...")
@POST("url1", ...)
class B extends Action { ... }

@GET(":*")
@Last
class Default extends Action { ... }

Для получения ссылки на контроллер в Action предусмотрен метод url, который генерирует GET ссылку с параметрами.

url[HelloAction]("name" -> "caiiiycuk")  // url/to/HelloAction?name=caiiiycuk

Ссылку на статические ресурсы из директории public или classpath можно получить с помощью методов publicUrl и resourceUrl соответственно. Поддерживаются классические перенаправления вроде forwardTo и redirectTo.

Разбор параметров

Xitrum позволяет прозрачно работать с тремя видами параметров:

  • uriParams — параметры после '?' (например: example.com/blah?x=1&y=2)
  • bodyParams — параметры переданные в теле POST запроса
  • pathParams — параметры закодированные в url (например: example.com/article/:id)

Доступ к параметрам осуществляется очень просто:

param("X")	// считать параметр X как String, бросить исключение если параметра нет
params("X")	// считать параметр X как List[String], бросить исключение если параметра нет
paramo("X")	// считать параметр X как Option[String]
paramso("X")	// считать параметр X как Option[List[String]]

param[Type]("X")	// считать параметр X как [Type], бросить исключение если параметра нет
params[Type]("X")	// считать параметр X как List[[Type]], бросить исключение если параметра нет
paramo[Type]("X")	// считать параметр X как Option[[Type]]
paramso[Type]("X")	// считать параметр X как Option[List[[Type]]]

pathParams задаются по аналогии с Rails с помощью символа ':' (:id, :article, :etc), дополнительно значения параметров можно ограничить с помощью регулярных выражений заключенных в '<>' (например, :id<[0-9]+>).

@GET("articles/:id<[0-9]+>", "articles/:id<[0-9]+>.:format")
class ArticlesShow extends Action {
  def execute() {
    val id     = param[Int]("id")
    val format = paramo("format").getOrElse("json")
    ...
  }
}

Иногда возникает необходимость считать бинарные данные тела POST запроса, делается это так:

val body = requestContentString		// результат String
val bodyMap = requestContentJson[Type]	// считать Json, результат Type
val raw = request.getContent		// результат ByteBuf

Шаблонизация

Сам по себе xitrum не имеет встроенного механизма шаблонизации, без шаблонизатора возможно генерировать следующие типы ответа:

  • respondText — ответить строкой «plain/text»
  • respondHtml — ответить строкой «text/html»
  • respondJson — преобразовать Scala объект в Json строку
  • respondBinary — бинарные данные
  • respondFile — отправить файл используя zero-copy (send-file)
  • Менее важные — respondJs, respondJsonP, respondJsonText, respondJsonPText, respondEventSource
Поддержка chunked response

Случается такая ситуация, когда ответ на запрос не помещается в памяти сервера. Например, наш сервер генерирует годовой отчет в CSV формате. Естественно в этой ситуации мы не можем сохранить весь отчет в памяти и отправить клиенту одним ответом. Жизненный цикл chunked response:

  1. Вызвать метод setChunked
  2. Вызвать respond*() столько раз, сколько необходимо
  3. Вызвать respondLastChunk когда все данные отправлены

val generator = new MyCsvGenerator

setChunked()

respondText(header, "text/csv")

while (generator.hasNextLine) {
  val line = generator.nextLine
  respondText(line)
}

respondLastChunk()

При использовании chunked response совместно с ActorAction можно очень просто реализовать Facebook BigPipe.

Для шаблонизации вы можете использовать Scalate, он подключен в шаблонном проекте. Шаблонизатор поддерживает несколько разных синтаксисов: mustache, scaml, jade и ssp. Я предпочитаю использовать ssp потому что он наиболее близок к html. В шаблонном проекте настроен jade, что бы сменить тип синтаксиса нужно в конфигурации xitrum.conf заменить строчку defaultType = jade на defaultType = ssp.

Возможности Scalate

  • HTML совместимый синтаксис (ssp)
  • HAML подобный синтаксис (jade)
  • Загрузка шаблонов на лету (во время выполнения)
  • Компилируемые шаблоны (проверка ошибок на этапе компиляции)
  • Включение шаблона в шаблон
  • Наследование шаблонов (возможность переопределения блоков)
  • Автоматическое экранирование тэгов
  • Использование Scala кода непосредственно в шаблоне

При использовании Scalate для каждого контроллера можно определить свое представление, по правилам Scalate путь до шаблона должен соответствовать пакету контроллера.

src/main/scala/quickstart/action/SiteIndex.scala  # класс контроллера
src/main/scalate/quickstart/action/SiteIndex.ssp  # шаблон контроллера
src/main/scalate/quickstart/action/SiteIndex/    # папка для фрагментов

package quickstart.action

import xitrum.annotation.GET

@GET("")
class SiteIndex extends DefaultLayout {
  def execute() {
    respondView()
  }
}

Как видите, что бы отобразить шаблон SiteIndex.ssp, достаточно вызвать respondView(). Предусмотрено понятие фрагмента, с помощью него можно менять представление контроллера.

@GET("")
class SiteIndex extends DefaultLayout {
  def execute() {
    respondHtml(renderFragment("some"))  # из папки фрагментов этого контроллера
  }
}

Xitrum не накладывает ограничений на строгое соответствие представления и контроллера, поэтому одно и то же представление может быть использовано в разных контролерах. Как следствие по умолчанию в шаблонах есть возможность пользоваться только методами из базового трейта Action. Передачу данных в шаблон можно осуществлять с помощью метода at.

Контроллер Шаблон
def execute() {
    at("login") = "caiiiycuk"
    at("rating") = 5
    respondView()
}

Hello ${at("login")}
You rating is ${at("rating")}

Если вы самостоятельно примите решение ограничиться одним шаблоном на контроллер, то очень полезным будет паттерн позволяющий импортировать текущий контроллер в шаблон. При его использовании методы контроллера могут быть прозрачно вызваны из шаблона.

Контроллер Шаблон
def random = Random.nextInt

def execute() {
    respondView()
}

<%
    val myAction = currentAction.asInstanceOf[MyAction]; import myAction._
%>

You random number is ${random}

Некоторый интерес представляет метод atJson, — он выполняет автоматическое преобразование моделей в Json, это оказывается очень полезным при передаче данных непосредственно в JavaScript.

Контроллер Шаблон
case class User(login: String, name: String)

...

def execute() {
  at("user") = User("admin", "Admin")
  respondView()
}

<script type="text/javascript">
  var user = ${atJson("user")};
  alert(user.login);
  alert(user.name);
</script>

Сессия и куки

Внутри контроллера для доступа к куки нужно использовать переменную requestCookies, а для установки новой куки соответственно responseCookies.

// Чтение
requestCookies.get("myCookie") match {
  case None         => ...
  case Some(string) => ...
}

// Установка
responseCookies.append(new DefaultCookie("name", "value"))

Xitrum автоматически обеспечивает сохранение, восстановление и шифрование сессии в куки. Работа с сессией осуществляется через переменную session.

session.clear  // очистить сессию
session("userId") = 1  // установить значение
session.isDefinedAt("userId")  // проверить существование
session("userId")  // считать из сессии

Фильтры

Обработкой запроса можно дополнительно управлять с помощью фильтров, всего их предусмотрено три: beforeFilter, afterFilter и aroundFilter. beforeFilter выполняется перед всякой обработкой запроса, если он возвращает false, то никакая дальнейшая обработка запроса данным контроллером выполнятся не будет. Напротив afterFilter выполняются последними.

before1 -true-> before2 -true-> +--------------------+ --> after1 --> after2
                                | around1 (1 of 2)   |
                                |   around2 (1 of 2) |
                                |     action         |
                                |   around2 (2 of 2) |
                                | around1 (2 of 2)   |
                                +--------------------+

Пример, определение языка интернационализации до обработки запроса.

beforeFilter {
  val lango: Option[String] = yourMethodToGetUserPreferenceLanguageInSession()
  lango match {
    case None       => autosetLanguage("ru", "en")
    case Some(lang) => setLanguage(lang)
  }
  true
}

def execute() { ... }

Кэширование

Итак, обработка запросов упрощенно выполняется следующим образом: (1) request -> (2) before фильтры -> (3) execute метод контроллера -> (4) after фильтры -> (5) response. Xitrum имеет встроенные возможности для кэширования всей цепочки обработки запроса (2 — 3 — 4 — 5) с помощью аннотации CachePageMinute и непосредственно метода execute (3), — аннотация CacheActionMinute. Время жизни кэша указывается в минутах. В кэш попадают только ответы со статусом 200 Ok.

import xitrum.Action
import xitrum.annotation.{GET, CacheActionMinute, CachePageMinute}

@GET("articles")
@CachePageMinute(1)
class ArticlesIndex extends Action {
  def execute() { ... }
}

@GET("articles/:id")
@CacheActionMinute(10)
class ArticlesShow extends Action {
  def execute() { ... }
}

По умолчанию фреймворк использует для кэширования свою реализацию Lru кэша. Однако, реализация механизма кэширования может быть легко изменена в конфигурации. Для кластеризованного кэша наиболее всего подойдет Hazelcast, способы подключения.

Кроме аннотаций, xitrum предоставляет доступ к объекту Cache. Его можно использовать для кэширования своих данных.

import xitrum.Config.xitrum.cache

// Cache with a prefix
val prefix = "articles/" + article.id
cache.put(prefix + "/likes", likes)
cache.put(prefix + "/comments", comments)

// Later, when something happens and you want to remove all cache related to the article
cache.remove(prefix)

Методы предоставляемые объектом Cache

  • put(key, value) — бессрочно поместить пару «ключ, значение» в кэш
  • putSecond, putMinute, putHour, putDay(key, value, interval) — значение будет удалено из кэша через указанный промежуток времени
  • putIfAbsent, putIfAbsentSecond, putIfAbsentMinute, putIfAbsentHour, putIfAbsentDay — тоже самое только значение в кэше не будет обновленно, если оно уже в нем содержится

RESTful API

Благодаря понятной маршрутизации реализация RESTful API тривиальна. Из коробки поддерживается документирование API с помощью Swagger

import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.{GET, Swagger}

@Swagger(
  Swagger.Note("Dimensions should not be bigger than 2000 x 2000")
  Swagger.OptStringQuery("text", "Text to render on the image, default: Placeholder"),
  Swagger.Response(200, "PNG image"),
  Swagger.Response(400, "Width or height is invalid or too big")
)
trait ImageApi extends Action with SkipCsrfCheck {
  lazy val text = paramo("text").getOrElse("Placeholder")
}

@GET("image/:width/:height")
@Swagger(  // <-- Наследуется от ImageApi
  Swagger.Summary("Generate rectangle image"),
  Swagger.IntPath("width"),
  Swagger.IntPath("height")
)
class RectImageApi extends Api {
  def execute {
    val width  = param[Int]("width")
    val height = param[Int]("height")
    // ...
  }
}

@GET("image/:width")
@Swagger(  // <-- Наследуется от ImageApi
  Swagger.Summary("Generate square image"),
  Swagger.IntPath("width")
)
class SquareImageApi extends Api {
  def execute {
    val width  = param[Int]("width")
    // ...
  }
}

Во время выполнения xitrum сгенерирует swagger.json который может быть использован в Swagger UI для удобного просмотра документации.

Важно: для всех POST запросов предусмотрена защита от CSRF атак, поэтому вы должны передавать csrf-token с любым POST запросом, либо, явно отключить эту защиту с помощью наследования от трейта SkipCsrfCheck. Подробнее про использование csrf-token.

Интернационализация

Интернационализация выполняется с помощью GNU gettext. У контроллера предусмотрен метод t для выполнения интернационализации.

def execute() {
  respondHtml(t("hello_world"))
}

Текущий язык перевода выбирается с помощью метода setLanguage, помимо этого можно использовать метод autosetLanguage для автоматического выбора языка в соответствии с Accept-Language браузера. Что бы получить шаблон pot, нужно выполнить sbt compile. Файлы с переводами нужно положить в classpath проекта (обычно в config/i18n). Если файл с переводом был изменен во время работы сервера, он будет перечитан и перевод применится без перезапуска сервера.

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

Оставшуюся часть материала вы всегда можете посмотреть на официальном сайте в учебнике. Вопросы которые я дополнительно хотел бы рассказать:

  • Интеграция с JRebel
  • Использование ActorAction
  • Postbacks
  • WebScoket, SockJS, EventSource
  • Deploy

Источники

Демонстрационный проект

Демонстрационный проект показывающий большую часть того на что способен xitrum (кажется он не очень полезен для обучения):

git clone https://github.com/ngocdaothanh/xitrum-demos.git
cd xitrum-demos
sbt/sbt run

Автор: Caiiiycuk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js