Pull to refresh

Пишем веб-сервис на Scalatra

Reading time7 min
Views9.4K
Scalatra – это легковесный высокопроизводительный web-фреймворк, близкий к Sinatra, что может значительно облегчить вам жизнь при переходе с Ruby на Scala. В этой статье я хочу восполнить пробел в отсутствии мануалов на русском языке по этому интересному фреймворку на примере создания простого приложения с возможностью аутентификации.

Установка


Официальная документация предлагает создать проект при помощи 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(). Аналогично можно получить и остальные параметры запроса. Если Вы извращенец хотите передать несколько параметров с одинаковым именем, например /user/52?foo=uno&bar=dos&baz=three&foo=anotherfoo (обратите внимание, что тут 2 раза встречается параметр foo), можно использовать функцию multiParams(), который позволяет единообразно обрабатывать параметры, например:

  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. Хотя данный фреймворк не пользуется большой популярностью в русскоязычном сообществе, широкий набор реализованной функциональности и простота освоения делают его весьма перспективным для написания как небольших веб-сервисов, так и крупных сайтов.

Если после прочтения статьи у вас остались какие-либо вопросы, готов ответить на них в комментариях, либо в следующих статьях.

Весь исходный код доступен на гитхабе.
Удачного изучения!
Tags:
Hubs:
+5
Comments9

Articles

Change theme settings