Scalatra – это легковесный высокопроизводительный web-фреймворк, близкий к Sinatra, что может значительно облегчить вам жизнь при переходе с Ruby на Scala. В этой статье я хочу восполнить пробел в отсутствии мануалов на русском языке по этому интересному фреймворку на примере создания простого приложения с возможностью аутентификации.
Официальная документация предлагает создать проект при помощи giter8 из заранее подготовленного шаблона. Однако если Вы хотите обойтись без лишних инструментов, можно просто создать sbt проект, следующим образом:
project\plugins.sbt
Этот плагин позволит вам запускать веб-сервис при помощи специальной sbt команды:
build.sbt
Назначение добавляемых библиотек можно понять из их названия, если Вам не нужен json или аутентификация — можете смело убрать лишнее.
Чтобы сервис начал отвечать на запросы, нужно для начала указать, какие контроллеры будут отвечать на запросы. Создадим для этого вот такой файл:
src\main\webapp\WEB-INF\web.xml
Если вы испытываете отвращение к xml, можно то же самое описать компактнее таким образом:
src/main/scala/ScalatraBootstrap.scala
Тут мы определили, что org.scalatra.example.UserController будет отвечать на запросы, начинающиеся с пути yoursite.example/user. Посмотрим, как устроен этот файл:
src\main\scala\org\scalatra\example\UserController.scala
Разберем этот код подробнее. Для начала все контроллеры в Scalatra должны наследоваться от ScalatraServlet. Чтобы определить пути, на по которым будет отвечать сервлет, нужно добавить блок get, post, put или delete (в зависимости от типа запроса), например:
будет отвечать на запросы к yoursite.example/user. Если какие-то из параметров являются частью URL, необходимо описать ваши параметры примерно так:
В результате внутри блока get можно использовать параметр id при помощи метода params(). Аналогично можно получить и остальные параметры запроса. Если Выизвращенец хотите передать несколько параметров с одинаковым именем, например /user/52?foo=uno&bar=dos&baz=three&foo=anotherfoo (обратите внимание, что тут 2 раза встречается параметр foo), можно использовать функцию multiParams(), который позволяет единообразно обрабатывать параметры, например:
Отмечу, что в UserController используется метод pass(). Он позволяет пропустить обработку по данному маршруту и перейти к следующим маршрутам (хотя в данном случае, больше нет обработчиков, под который попадает данный путь). Если требуется прервать обработку запроса и показать пользователю страницу с ошибкой следует использовать метод halt(), который умеет принимать различные параметры, например код возврата и текст ошибки.
Еще одна возможность, предоставляемая фреймворком — задать пред- и пост-обработчики, например, написав:
можно задать тип ответа (в данном случае json) и затребовать у пользователя аутентификацию (об аутентификации и работе с json речь пойдет в следующих разделах).
Более подробную информацию про маршрутизацию можно найти в официальной документации.
В предыдущем разделе в качестве ответа контроллера используются объекты, получаемые из класса BD. Однако в Scalatra нет встроенного фреймворка для работы с базой данных, в связи с чем я оставил лишь имитацию работы с БД.
src\main\scala\org\scalatra\example\DB.scala
src\main\scala\org\scalatra\example\models\User.scala
Однако, не думайте, что с этим есть какие-либо сложности — в официальной документациии описано, как подружить Scalatra с наиболее популярными базами данных и ORM: Slick, MongoDB, Squeryl, Riak.
Обратите внимание, что контроллер возвращает напрямую case class User, а точнее даже Option[User] и List[User]. По умолчанию Scalatra преобразует возвращаемое значение в строку и использует ее в качестве ответа на запрос, т.е., например, ответ на запрос /user будет таким:
Для того, чтобы сервлет начал работать с json, необходимо:
После выполнения этих простых действий ответ на тот же запрос /user станет таким:
Напоследок, хотелось бы коснуться такой темы, как аутентификация пользователей. Для этого предлагается использовать Scentry фреймворк, который представляет из себя портированный на Scala фреймворк Warden, что также может облегчить жизнь людям, знакомым с Ruby.
Если внимательно посмотреть на класс UserController, можно обнаружить, что аутентификация в нем уже реализована. Для этого к классу подмешан трейт AuthenticationSupport и в before() фильтре вызван метод basicAuth(). Взглянем на реализацию AuthenticationSupport.
src\main\scala\org\scalatra\example\AuthenticationSupport.scala
Первое, что нужно сделать — это определить стратегию аутентификации — класс, реализующий интерфейс ScentryStrategy. В данном случае мы использовали заготовку BasicAuthStrategy[User] реализующий некоторые стандартные методы. После этого нам осталось определить 2 метода — validate(), который в случае успешного логина должен возвращать Some[User], либо None в случае неверных данных и getUserId(), который должен возвращать строку для дальнейшего ее добавления в заголовки ответа.
Следующее, что нужно сделать — это объединить OurBasicAuthStrategy и ScentrySupport в трейт AuthenticationSupport, который мы и будем подмешивать к контроллеру. В нем мы зарегистрировали наше стратегию аутентификации и реализовали (наиболее простым способом) способы получения объекта пользователя из сессии и, наоборот, добавления его id в сессию.
В результате, если незалогинившийся пользователь зайдет на страницу, за обработку которой отвечает UserController, ему сначала нужно будет ввести логин и пароль.
В данной статье были показаны лишь некоторые, выборочные возможности Scalatra. Хотя данный фреймворк не пользуется большой популярностью в русскоязычном сообществе, широкий набор реализованной функциональности и простота освоения делают его весьма перспективным для написания как небольших веб-сервисов, так и крупных сайтов.
Если после прочтения статьи у вас остались какие-либо вопросы, готов ответить на них в комментариях, либо в следующих статьях.
Весь исходный код доступен на гитхабе.
Удачного изучения!
Установка
Официальная документация предлагает создать проект при помощи giter8 из заранее подготовленного шаблона. Однако если Вы хотите обойтись без лишних инструментов, можно просто создать sbt проект, следующим образом:
project\plugins.sbt
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.1.0")
Этот плагин позволит вам запускать веб-сервис при помощи специальной sbt команды:
$ sbt
> container:start
build.sbt
val scalatraVersion = "2.4.0-RC2-2"
resolvers += "Scalaz Bintray Repo" at "https://dl.bintray.com/scalaz/releases"
lazy val root = (project in file(".")).settings(
organization := "com.example",
name := "scalatra-auth-example",
version := "0.1.0-SNAPSHOT",
scalaVersion := "2.11.6",
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature"),
libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra-auth" % scalatraVersion,
"org.scalatra" %% "scalatra" % scalatraVersion,
"org.scalatra" %% "scalatra-json" % scalatraVersion,
"org.scalatra" %% "scalatra-specs2" % scalatraVersion % "test",
"org.json4s" %% "json4s-jackson" % "3.3.0.RC2",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided"
)
).settings(jetty(): _*)
Назначение добавляемых библиотек можно понять из их названия, если Вам не нужен json или аутентификация — можете смело убрать лишнее.
Маршрутизация
Чтобы сервис начал отвечать на запросы, нужно для начала указать, какие контроллеры будут отвечать на запросы. Создадим для этого вот такой файл:
src\main\webapp\WEB-INF\web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>user</servlet-name>
<servlet-class>
org.scalatra.example.UserController
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>user</servlet-name>
<url-pattern>/user/*</url-pattern>
</servlet-mapping>
</web-app>
Если вы испытываете отвращение к xml, можно то же самое описать компактнее таким образом:
src/main/scala/ScalatraBootstrap.scala
import org.scalatra.example._
import org.scalatra._
import javax.servlet.ServletContext
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
context.mount(new UserController, "/user")
}
}
Тут мы определили, что org.scalatra.example.UserController будет отвечать на запросы, начинающиеся с пути yoursite.example/user. Посмотрим, как устроен этот файл:
src\main\scala\org\scalatra\example\UserController.scala
package org.scalatra.example
import org.json4s.{DefaultFormats, Formats}
import org.scalatra._
import org.scalatra.json.JacksonJsonSupport
import scala.util.{Failure, Success, Try}
class UserController extends ScalatraServlet with AuthenticationSupport with JacksonJsonSupport {
protected implicit lazy val jsonFormats: Formats = DefaultFormats
before() {
contentType = formats("json")
basicAuth()
}
get("/") {
DB.getAllUsers
}
get("/:id") {
Try {
params("id").toInt
} match {
case Success(id) => DB.getUserById(id)
case Failure(ex) => pass()
}
}
}
Разберем этот код подробнее. Для начала все контроллеры в Scalatra должны наследоваться от ScalatraServlet. Чтобы определить пути, на по которым будет отвечать сервлет, нужно добавить блок get, post, put или delete (в зависимости от типа запроса), например:
get("/") { /*...*/ }
будет отвечать на запросы к yoursite.example/user. Если какие-то из параметров являются частью URL, необходимо описать ваши параметры примерно так:
get("/:id") { params("id") }
В результате внутри блока get можно использовать параметр id при помощи метода params(). Аналогично можно получить и остальные параметры запроса. Если Вы
multiParams("id") // => Seq("52")
multiParams("foo") // => Seq("uno", "anotherfoo")
multiParams("unknown") // => an empty Seq
Отмечу, что в UserController используется метод pass(). Он позволяет пропустить обработку по данному маршруту и перейти к следующим маршрутам (хотя в данном случае, больше нет обработчиков, под который попадает данный путь). Если требуется прервать обработку запроса и показать пользователю страницу с ошибкой следует использовать метод halt(), который умеет принимать различные параметры, например код возврата и текст ошибки.
Еще одна возможность, предоставляемая фреймворком — задать пред- и пост-обработчики, например, написав:
before() {
contentType = formats("json")
basicAuth()
}
можно задать тип ответа (в данном случае json) и затребовать у пользователя аутентификацию (об аутентификации и работе с json речь пойдет в следующих разделах).
Более подробную информацию про маршрутизацию можно найти в официальной документации.
Работа с БД
В предыдущем разделе в качестве ответа контроллера используются объекты, получаемые из класса BD. Однако в Scalatra нет встроенного фреймворка для работы с базой данных, в связи с чем я оставил лишь имитацию работы с БД.
src\main\scala\org\scalatra\example\DB.scala
package org.scalatra.example
import org.scalatra.example.models.User
object DB {
private var users = List(
User(1, "scalatra", "scalatra"),
User(2, "admin", "admin"))
def getAllUsers: List[User] = users
def getUserById(id: Int): Option[User] = users.find(_.id == id)
def getUserByLogin(login: String): Option[User] = users.find(_.login == login)
}
src\main\scala\org\scalatra\example\models\User.scala
package org.scalatra.example.models
case class User(id: Int, login:String, password: String)
Однако, не думайте, что с этим есть какие-либо сложности — в официальной документациии описано, как подружить Scalatra с наиболее популярными базами данных и ORM: Slick, MongoDB, Squeryl, Riak.
Json
Обратите внимание, что контроллер возвращает напрямую case class User, а точнее даже Option[User] и List[User]. По умолчанию Scalatra преобразует возвращаемое значение в строку и использует ее в качестве ответа на запрос, т.е., например, ответ на запрос /user будет таким:
List(User(1,scalatra,scalatra), User(2,admin,admin)).
Для того, чтобы сервлет начал работать с json, необходимо:
- Подмешать к нему трейт JacksonJsonSupport
- Указать формат преобразования к json. Scalatra использует json4s для работы с json, что позволяет создавать кастомные правила преобразования в json и обратно. В нашем случае будет достаточно формата по умолчанию:
protected implicit lazy val jsonFormats: Formats = DefaultFormats
- Добавить заголовок с тип возвращаемого значения:
contentType = formats("json")
После выполнения этих простых действий ответ на тот же запрос /user станет таким:
[{"id":1,"login":"scalatra","password":"scalatra"},{"id":2,"login":"admin","password":"admin"}]
Аутентификация
Напоследок, хотелось бы коснуться такой темы, как аутентификация пользователей. Для этого предлагается использовать Scentry фреймворк, который представляет из себя портированный на Scala фреймворк Warden, что также может облегчить жизнь людям, знакомым с Ruby.
Если внимательно посмотреть на класс UserController, можно обнаружить, что аутентификация в нем уже реализована. Для этого к классу подмешан трейт AuthenticationSupport и в before() фильтре вызван метод basicAuth(). Взглянем на реализацию AuthenticationSupport.
src\main\scala\org\scalatra\example\AuthenticationSupport.scala
package org.scalatra.example
import org.scalatra.auth.strategy.{BasicAuthStrategy, BasicAuthSupport}
import org.scalatra.auth.{ScentrySupport, ScentryConfig}
import org.scalatra.example.models.User
import org.scalatra.ScalatraBase
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
class OurBasicAuthStrategy(protected override val app: ScalatraBase, realm: String) extends BasicAuthStrategy[User](app, realm) {
protected def validate(userName: String, password: String)(implicit request: HttpServletRequest, response: HttpServletResponse): Option[User] = {
DB.getUserByLogin(userName).filter(_.password == password)
}
protected def getUserId(user: User)(implicit request: HttpServletRequest, response: HttpServletResponse): String = user.id.toString
}
trait AuthenticationSupport extends ScentrySupport[User] with BasicAuthSupport[User] {
self: ScalatraBase =>
val realm = "Scalatra Basic Auth Example"
protected def fromSession = {
case id: String => DB.getUserById(id.toInt).get
}
protected def toSession = {
case usr: User => usr.id.toString
}
protected val scentryConfig = new ScentryConfig {}.asInstanceOf[ScentryConfiguration]
override protected def configureScentry() = {
scentry.unauthenticated {
scentry.strategies("Basic").unauthenticated()
}
}
override protected def registerAuthStrategies() = {
scentry.register("Basic", app => new OurBasicAuthStrategy(app, realm))
}
}
Первое, что нужно сделать — это определить стратегию аутентификации — класс, реализующий интерфейс ScentryStrategy. В данном случае мы использовали заготовку BasicAuthStrategy[User] реализующий некоторые стандартные методы. После этого нам осталось определить 2 метода — validate(), который в случае успешного логина должен возвращать Some[User], либо None в случае неверных данных и getUserId(), который должен возвращать строку для дальнейшего ее добавления в заголовки ответа.
Следующее, что нужно сделать — это объединить OurBasicAuthStrategy и ScentrySupport в трейт AuthenticationSupport, который мы и будем подмешивать к контроллеру. В нем мы зарегистрировали наше стратегию аутентификации и реализовали (наиболее простым способом) способы получения объекта пользователя из сессии и, наоборот, добавления его id в сессию.
В результате, если незалогинившийся пользователь зайдет на страницу, за обработку которой отвечает UserController, ему сначала нужно будет ввести логин и пароль.
Заключение
В данной статье были показаны лишь некоторые, выборочные возможности Scalatra. Хотя данный фреймворк не пользуется большой популярностью в русскоязычном сообществе, широкий набор реализованной функциональности и простота освоения делают его весьма перспективным для написания как небольших веб-сервисов, так и крупных сайтов.
Если после прочтения статьи у вас остались какие-либо вопросы, готов ответить на них в комментариях, либо в следующих статьях.
Весь исходный код доступен на гитхабе.
Удачного изучения!