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 

Какой фреймворк вы используете (Scala)

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/214913/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *