- PVSM.RU - https://www.pvsm.ru -
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 со статической типизацией, восхитительно!
Проект родом из Японии и имеет обстоятельную документацию. Разработчики проекта оставляют очень приятное впечатление, всегда прислушиваются к тому, что пишут в официальной группе и очень быстро закрывают баги. Вот уже почти год как я не получил ни одного отказа или зависшей задачи в трекере.
Я начал разрабатывать Xitrum летом 2010 года, для использования в реальных проектах компании Mobilus [1]. В то время, Play поддерживал только Java, а Lift был единственным полноценным фреймворком для Scala. Мы пытались его использовать несколько месяцев, но оказалось, что он не так прост, по крайней мере для нас знакомых с разработкой на Rails. Поэтому, как технический руководитель, я принял решение создать быстрый и масштабируемый веб-фреймворк на Scala для моей команды, настолько же простой в использовании, как и Rails. На самом деле, результат оказался больше похоже на Merb, нежели чем на Rails (в xitrum отсутствует слой доступа к данным).
С течением времени многие люди поучаствовали в разработки фреймворка. На данный момент команда, разрабатывающая ядро Xitrum состоит из двух человек: Oshida [2] и Ngoc [3].
Итак, xitrum:
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 [4]).
Структура директории проекта:
./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.
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 позволяет прозрачно работать с тремя видами параметров:
Доступ к параметрам осуществляется очень просто:
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 не имеет встроенного механизма шаблонизации, без шаблонизатора возможно генерировать следующие типы ответа:
val generator = new MyCsvGenerator
setChunked()
respondText(header, "text/csv")
while (generator.hasNextLine) {
val line = generator.nextLine
respondText(line)
}
respondLastChunk()
При использовании chunked response совместно с ActorAction можно очень просто реализовать Facebook BigPipe [5].
Для шаблонизации вы можете использовать Scalate [6], он подключен в шаблонном проекте. Шаблонизатор поддерживает несколько разных синтаксисов: mustache, scaml, jade и ssp. Я предпочитаю использовать ssp потому что он наиболее близок к html. В шаблонном проекте настроен jade, что бы сменить тип синтаксиса нужно в конфигурации xitrum.conf заменить строчку defaultType = jade на defaultType = ssp.
При использовании 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.
| Контроллер | Шаблон |
|---|---|
|
|
Если вы самостоятельно примите решение ограничиться одним шаблоном на контроллер, то очень полезным будет паттерн позволяющий импортировать текущий контроллер в шаблон. При его использовании методы контроллера могут быть прозрачно вызваны из шаблона.
| Контроллер | Шаблон |
|---|---|
|
|
Некоторый интерес представляет метод atJson, — он выполняет автоматическое преобразование моделей в Json, это оказывается очень полезным при передаче данных непосредственно в JavaScript.
| Контроллер | Шаблон |
|---|---|
|
|
Внутри контроллера для доступа к куки нужно использовать переменную 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, способы подключения [7].
Кроме аннотаций, 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
Благодаря понятной маршрутизации реализация 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 [8].
def execute() {
respondHtml(t("hello_world"))
}
Текущий язык перевода выбирается с помощью метода setLanguage, помимо этого можно использовать метод autosetLanguage для автоматического выбора языка в соответствии с Accept-Language браузера. Что бы получить шаблон pot, нужно выполнить sbt compile. Файлы с переводами нужно положить в classpath проекта (обычно в config/i18n). Если файл с переводом был изменен во время работы сервера, он будет перечитан и перевод применится без перезапуска сервера.
Если вы прочли эту статью до конца, то полагаю теперь вы знает порядка 70% функционала фреймворка. И мне кажется, это подтверждает мою мысль о том, что порог вхождения очень низок. Поэтому, рекомендую пробовать и задавать вопросы.
Оставшуюся часть материала вы всегда можете посмотреть на официальном сайте в учебнике. Вопросы которые я дополнительно хотел бы рассказать:
Источники
git clone https://github.com/ngocdaothanh/xitrum-demos.git
cd xitrum-demos
sbt/sbt run
Автор: Caiiiycuk
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/57344
Ссылки в тексте:
[1] Mobilus: http://mobilus.co.jp
[2] Oshida: https://github.com/georgeOsdDev
[3] Ngoc: https://github.com/ngocdaothanh
[4] sbt-eclipse#182: https://github.com/typesafehub/sbteclipse/issues/182
[5] Facebook BigPipe: http://www.cubrid.org/blog/dev-platform/faster-web-page-loading-with-facebook-bigpipe/
[6] Scalate: http://scalate.fusesource.org/
[7] способы подключения: https://github.com/ngocdaothanh/xitrum-hazelcast
[8] csrf-token: http://ngocdaothanh.github.io/xitrum/guide/restful.html#anti-csrf
[9] Документация: http://ngocdaothanh.github.io/xitrum/guide/index.html
[10] Гугл группа: https://groups.google.com/group/xitrum-framework
[11] Официальный сайт: http://ngocdaothanh.github.io/xitrum/
[12] Источник: http://habrahabr.ru/post/214913/
Нажмите здесь для печати.