...

среда, 19 марта 2014 г.

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 http://ift.tt/POjask 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).

@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 http://ift.tt/1lNusqA
cd xitrum-demos
sbt/sbt run





This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий