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. В то время, 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
- Вызвать метод setChunked
- Вызвать respond*() столько раз, сколько необходимо
- Вызвать 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.
- 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.
Контроллер | Шаблон |
---|---|
|
|
Если вы самостоятельно примите решение ограничиться одним шаблоном на контроллер, то очень полезным будет паттерн позволяющий импортировать текущий контроллер в шаблон. При его использовании методы контроллера могут быть прозрачно вызваны из шаблона.
Контроллер | Шаблон |
---|---|
|
|
Некоторый интерес представляет метод 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, способы подключения.
Кроме аннотаций, 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.
def execute() {
respondHtml(t("hello_world"))
}
Текущий язык перевода выбирается с помощью метода setLanguage, помимо этого можно использовать метод autosetLanguage для автоматического выбора языка в соответствии с Accept-Language браузера. Что бы получить шаблон pot, нужно выполнить sbt compile. Файлы с переводами нужно положить в classpath проекта (обычно в config/i18n). Если файл с переводом был изменен во время работы сервера, он будет перечитан и перевод применится без перезапуска сервера.
Если вы прочли эту статью до конца, то полагаю теперь вы знает порядка 70% функционала фреймворка. И мне кажется, это подтверждает мою мысль о том, что порог вхождения очень низок. Поэтому, рекомендую пробовать и задавать вопросы.
Оставшуюся часть материала вы всегда можете посмотреть на официальном сайте в учебнике. Вопросы которые я дополнительно хотел бы рассказать:
- Интеграция с JRebel
- Использование ActorAction
- Postbacks
- WebScoket, SockJS, EventSource
- Deploy
Источники
git clone https://github.com/ngocdaothanh/xitrum-demos.git
cd xitrum-demos
sbt/sbt run
Автор: Caiiiycuk