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

от автора

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/262603/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *