Статьи

Play 2.0: Акка, Отдых, Json и зависимости

Последние пару месяцев я все больше и больше погружаюсь в скалу. Scala вместе с «Play Framework» предоставляет вам очень эффективную и быструю среду разработки (то есть, как только вы поймете особенности языка Scala).

Ребята из Play Play усердно трудятся над новой версией Play 2.0. В Play 2.0 scala играет гораздо более важную роль, и особенно процесс сборки был значительно улучшен. Единственная проблема, с которой я столкнулся в Play 2.0, — это отсутствие хорошей документации. Ребята усердно работают над обновлением вики, но зачастую все еще приходится много проб и ошибок, чтобы получить то, что вы хотите. Обратите внимание, что часто это происходит не только из-за Play, но иногда я все еще борюсь с более экзотическими конструкциями Scala 😉

В этой статье я познакомлю вас с тем, как вы можете выполнять некоторые общие задачи в Play 2.0 с помощью Scala. Более конкретно я покажу вам, как создать приложение, которое:

  • использует управление зависимостями на основе sbt для настройки внешних зависимостей
  • редактируется в Eclipse (с плагином Scala-ide) с помощью команды play eclipsify
  • предоставляет Rest API, используя маршруты Play
  • использует Akka 2.0 (предоставляемый платформой Play) для асинхронного вызова базы данных и генерации Json (только потому, что мы можем)
  • конвертировать объекты Scala в Json, используя предоставленную Play функцию Json (на основе jerkson)

Я не буду показывать доступ к базе данных с помощью Querulous, если вы хотите узнать больше об этом взгляде на эту статью. Я хотел бы преобразовать код Querulous в Anorm. Но так как мой последний опыт с Anorm был, как бы это выразиться, не убедительно положительным, я оставлю это на потом.

Создание приложения с помощью Play 2.0

Начать работать с Play 2.0 очень легко и хорошо документировано, поэтому я не буду тратить на это слишком много времени. Для полной инструкции смотрите Play 2.0 Wiki . Чтобы начать работу, после того, как вы загрузили и извлекли Play 2.0, сделайте следующее:

Выполните следующую команду из консоли:

1
$play new FirstStepsWithPlay20

Это создаст новый проект и покажет вам что-то вроде следующего:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/
  
play! 2.0-RC2, http://www.playframework.org
  
The new application will be created in /Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay20
  
What is the application name?
> FirstStepsWithPlay20
  
Which template do you want to use for this new application?
  
  1 - Create a simple Scala application
  2 - Create a simple Java application
  3 - Create an empty project
  
> 1
  
OK, application FirstStepsWithPlay20 is created.
  
Have fun!

Теперь у вас есть приложение, которое вы можете запустить. Перейдите в только что созданный каталог и выполните play run.

01
02
03
04
05
06
07
08
09
10
$ play run
  
[info] Loading project definition from /Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay2/project
[info] Set current project to FirstStepsWithPlay2 (in build file:/Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay2/)
  
--- (Running the application from SBT, auto-reloading is enabled) ---
  
[info] play - Listening for HTTP on port 9000...
  
(Server started, use Ctrl+D to stop and go back to the console...)

Если вы перейдете по адресу http: // localhost: 9000 , вы увидите свое первое приложение Play 2.0. И вы закончили с базовой установкой Play 2.0.

Управление зависимостями

Я упоминал во введении, что я не начинал этот проект с нуля. Я переписал службу отдыха, которую я сделал с помощью Play 1.2.4, Akka 1.x, JAX-RS и Json-Lift, на компоненты, предоставляемые платформой Play 2.0. Так как управление зависимостями изменилось между Play 1.2.4 и Play 2.0, мне нужно было настроить мой новый проект с нужными мне зависимостями. В Play 2.0 вы делаете это в файле с именем build.scala, который вы можете найти в папке проекта в вашем проекте. После добавления зависимостей из моего предыдущего проекта этот файл выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sbt._
import Keys._
import PlayProject._
  
object ApplicationBuild extends Build {
  
    val appName         = "FirstStepsWithPlay2"
    val appVersion      = "1.0-SNAPSHOT"
  
    val appDependencies = Seq(
      "com.twitter" % "querulous" % "2.6.5" ,
      "net.liftweb" %% "lift-json" % "2.4" ,
      "com.sun.jersey" % "jersey-server" % "1.4" ,
      "com.sun.jersey" % "jersey-core" % "1.4" ,
      "postgresql" % "postgresql" % "9.1-901.jdbc4"
    )
  
    val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
      // Add extra resolver for the twitter 
        resolvers += "Twitter repo" at "http://maven.twttr.com/" ,
        resolvers += "DevJava repo" at "http://download.java.net/maven/2/"
    )
}

Как использовать этот файл довольно просто, как только вы прочитаете документацию sbt (http://code.google.com/p/simple-build-tool/wiki/LibraryManagement ). По сути, мы определяем библиотеки, которые хотим, используя appDependencies, и определяем некоторые дополнительные репозитории, из которых sbt должен загружать свои зависимости (используя распознаватели). Приятно отметить, что вы можете указать %% при определении зависимостей. Это подразумевает, что мы также хотим найти библиотеку, которая соответствует нашей версии scala. SBT просматривает нашу текущую настроенную версию и добавляет квалификатор для этой версии. Это гарантирует, что мы получим версию, которая работает для нашей версии Scala.
Как я уже упоминал, я хотел заменить большинство внешних библиотек функциональностью Play 2.0. После удаления материала, который я больше не использовал, этот файл выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import sbt._
import Keys._
import PlayProject._
  
object ApplicationBuild extends Build {
  
    val appName         = "FirstStepsWithPlay2"
    val appVersion      = "1.0-SNAPSHOT"
  
    val appDependencies = Seq(
      "com.twitter" % "querulous" % "2.6.5" ,
      "postgresql" % "postgresql" % "9.1-901.jdbc4"
    )
  
    val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
      // Add extra resolver for the twitter 
        resolvers += "Twitter repo" at "http://maven.twttr.com/"
    )
}

С настроенными зависимостями я могу настроить этот проект для своей IDE. Хотя все мои коллеги являются большими сторонниками IntelliJ, я все еще возвращаюсь к тому, к чему я привык: Eclipse. Итак, давайте посмотрим, что вам нужно сделать, чтобы запустить этот проект в Eclipse.

Работа от Eclipse

В моей версии Eclipse у меня установлен плагин scala , и платформа Play 2.0 прекрасно работает вместе с этим плагином. Чтобы получить ваш проект в eclipse, все, что вам нужно сделать, это запустить следующую команду: play eclipsify

1
2
3
4
5
6
7
jos@Joss-MacBook-Pro.local:~/dev/play-2.0-RC2/FirstStepsWithPlay2$ ../play eclipsify
[info] Loading project definition from /Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay2/project
[info] Set current project to FirstStepsWithPlay2 (in build file:/Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay2/)
[info] About to create Eclipse project files for your project(s).
[info] Compiling 1 Scala source to /Users/jos/Dev/play-2.0-RC2/FirstStepsWithPlay2/target/scala-2.9.1/classes...
[info] Successfully created Eclipse project files for project(s): FirstStepsWithPlay2
jos@Joss-MacBook-Pro.local:~/dev/play-2.0-RC2/FirstStepsWithPlay2$

Теперь вы можете использовать «импорт проекта» из Eclipse и редактировать проект Play 2.0 / Scala непосредственно из Eclipse. Можно запустить среду Play непосредственно из Eclipse, но я этим не пользовался. Я просто запускаю проект Play из командной строки, и все изменения, которые я делаю в Eclipse, сразу становятся видимыми. Для тех из вас, кто работал с Play дольше, это, вероятно, уже не так уж и особенное. Лично я все еще поражен производительностью этой среды.

предоставляет Rest API, используя маршруты Play

В моем предыдущем проекте Play я использовал модуль jersey, чтобы иметь возможность использовать аннотации JAX-RS для указания моего Rest API. Поскольку Play 2.0 содержит множество критических изменений API и в значительной степени переписан с нуля, нельзя ожидать, что все старые модули будут работать. Это также относится к модулю Джерси. Я углубился в код этого модуля, чтобы увидеть, были ли изменения тривиальными, но так как я не смог найти никакой документации о том, как создать плагин для Play 2.0, который позволяет вам взаимодействовать с обработкой маршрута, я решил просто переключиться на способ Play 2.0 делает отдых. А с помощью файла «маршрутов» было очень легко соединить (просто) две операции, которые я представил, с простым контроллером:

1
2
3
4
5
6
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
  
GET     /resources/rest/geo/list    controllers.Application.processGetAllRequest
GET     /resources/rest/geo/:id     controllers.Application.processGetSingleRequest(id:String)

Соответствующий контроллер выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package controllers
  
import akkawebtemplate.GeoJsonService
import play.api.mvc.Action
import play.api.mvc.Controller
  
object Application extends Controller {
  
  val service = new GeoJsonService()
  
  def processGetSingleRequest(code: String) = Action {
    val result = service.processGetSingleRequest(code)
    Ok(result).as("application/json")
  }
  
  def processGetAllRequest() = Action {
    val result = service.processGetAllRequest;
    Ok(result).as("application/json");
  }
}

Как видите, я только что создал очень простые, базовые действия. Пока не рассматривали обработку ошибок и исключений, но API Rest, предлагаемый Play, действительно делает ненужным использование дополнительной среды Rest. Это первый из рамок. Следующей частью моего исходного приложения, которую нужно было изменить, был код Akka. Play 2.0 включает в себя последнюю версию библиотеки Akka (2.0-RC1). Так как мой оригинальный код Akka был написан против 1.2.4, было много конфликтов. Обновление исходного кода было не так просто.

Использование Akka 2.0

Я не буду вдаваться во все проблемы, которые у меня были с Akka 2.0. Самая большая проблема заключалась в очень дрянной документации на Play Wiki и дрянной документации на веб-сайте Akka, и мои дурацкие умения находить правильную информацию в документации Akka. Вместе со мной использование Akka в течение трех или четырех месяцев не делает его лучшей комбинацией. После нескольких часов фрустрации я просто удалил весь существующий код Akka и начал с нуля. Через 20 минут у меня все заработало, работая с Akka 2 и используя мастер-конфигурацию из Play. В следующем листинге вы можете увидеть соответствующий код (я намеренно оставил импорт, так как во многих примерах вы можете найти их, они опущены, что облегчает работу, которая намного сложнее)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import akka.actor.actorRef2Scala
import akka.actor.Actor
import akka.actor.Props
import akka.dispatch.Await
import akka.pattern.ask
import akka.util.duration.intToDurationInt
import akka.util.Timeout
import model.GeoRecord
import play.libs.Akka
import resources.commands.Command
import resources.commands.FULL
import resources.commands.SINGLE
import resources.Database
  
/**
 * This actor is responsible for returning JSON objects from the database. It uses querulous to
 * query the database and parses the result into the GeoRecord class.
 */
class JsonActor extends Actor {
  
  /**
   * Based on the type recieved we determine what command to execute, most case classes
   * can be executed using the normal two steps. Execute a query, convert result to
   * a set of json data and return this result.
   */
  def receive = {
  
    // when we receive a Command we process it and return the result
    case some: Command => {
  
      // execute the query from the FULL command and process the results using the
      // processRows function
      var records:Seq[GeoRecord] = null;
  
      // if the match parameter is null we do the normal query, if not we pass in a set of varargs
      some.parameters match {
        case null =>  records = Database.getQueryEvaluator.select(some.query) {some.processRows}
        case _ => records = Database.getQueryEvaluator.select(some.query, some.parameters:_*) {some.processRows}
      }
      // return the result as a json string
      sender ! some.toJson(records)
    }
  
    case _ => sender ! null
  }
}
  
/**
 * Handle the specified path. This rest service delegates the functionality to a specific actor
 * and if the result from this actor isn't null return the result
 */
class GeoJsonService {
  
  def processGetSingleRequest(code: String) = {
      val command = SINGLE();
      command.parameters = List(code);
      runCommand(command);
  }
  
  /**
   * Operation that handles the list REST command. This creates a command
   * that forwards to the actor to be executed.
   */
  def processGetAllRequest:String = {
   runCommand(FULL());
  }
  
  /**
   * Function that runs a command on one of the actors and sets the response
   */
  private def runCommand(command: Command):String =  {
  
    // get the actor
    val actor = Akka.system.actorOf(Props[JsonActor])
    implicit val timeout = Timeout(5 seconds)
    val result = Await.result(actor ? command, timeout.duration).asInstanceOf[String]
    // return result as String
    result
  }
}

Много кода, но я хотел показать вам определение актера и как их использовать. Подводя итог, код Akka 2.0, который вам нужно использовать, чтобы выполнить шаблон запроса / ответа с помощью Akka, выглядит так:

1
2
3
4
5
6
7
8
9
private def runCommand(command: Command):String =  {
  
    // get the actor
    val actor = Akka.system.actorOf(Props[JsonActor])
    implicit val timeout = Timeout(5 seconds)
    val result = Await.result(actor ? command, timeout.duration).asInstanceOf[String]
    // return result as String
    result
  }

При этом используется глобальная конфигурация Akka для извлечения актера требуемого типа. Затем мы посылаем команду актеру и возвращаем Future, на котором мы ждем 5 секунд результата, который мы приводим к String. Это будущее ждет нашего актера, чтобы отправить ответ. Это делается в самом актере:

1
sender ! some.toJson(records)

С заменой Акки я наконец-то снова получил работающую систему. Просматривая документацию по Play 2.0, я заметил, что они предоставляют собственную библиотеку Json, начиная с 2.0. Поскольку я использовал Json-Lift в предыдущей версии, я подумал, что было бы неплохо перенести этот код в библиотеку Json с именем Jerkson, предоставляемую Play.

Переезд в Джерксон

Переезд в новую библиотеку был довольно легким. И Lift-Json, и Jerkson используют почти одинаковую концепцию построения объектов Json. В старой версии я не использовал автоматическую сортировку (так как я должен был соответствовать формату jsongeo), поэтому в этой версии я также выполнял сортировку вручную. В следующем листинге вы можете увидеть старую версию и новую версию вместе. Как вы можете видеть, концепции, используемые в обоих, в значительной степени одинаковы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#New version using jerkson
    val jsonstring = JsObject(
      List("type" -> JsString("featureCollection"),
        "features" -> JsArray(
          records.map(r =>
            (JsObject(List(
              "type" -> JsString("Feature"),
              "gm_naam" -> JsString(r.name),
              "geometry" -> Json.parse(r.geojson),
              "properties" -> ({
                var toAdd = List[(String, play.api.libs.json.JsValue)]()
                r.properties.foreach(entry => (toAdd ::= entry._1 -> JsString(entry._2)))
                JsObject(toAdd)
              })))))
            .toList)))
  
#Old version using Lift-Json
    val json =
      ("type" -> "featureCollection") ~
        ("features" -> records.map(r =>
          (("type" -> "Feature") ~
            ("gm_naam" -> r.name) ~
            ("geometry" -> parse(r.geojson)) ~
            ("properties" -> ({
              // create an empty object
              var obj = JNothing(0)
              // iterate over the properties
              r.properties.foreach(entry => (
                // add each property to the object, the reason
                // we do this is, that else it results in an
                // arraylist, not a list of seperate properties
                obj = concat(obj, JField(entry._1, entry._2))))
              obj
            })))))

И после всего этого у меня точно так же, как у меня уже было. Но теперь с Play 2.0 и без использования каких-либо внешних библиотек (кроме Querulous). Пока что мой опыт с Play 2.0 был очень позитивным. Отсутствие хороших конкретных примеров и документации иногда может раздражать, но это понятно. Они предоставляют несколько обширных примеров в своем распространении, но ничего, что соответствовало моим сценариям использования. Так что снимаю шляпу перед парнями, которые отвечают за Play 2.0. То, что я видел до сих пор, отличную и всеобъемлющую структуру, множество функциональных возможностей и отличную среду для программирования Scala. В ближайшие пару недель я посмотрю, смогу ли я набраться смелости, чтобы начать работать с Anorm, и я посмотрите, что Play может предложить на стороне клиента. До сих пор я смотрел на LESS, который мне действительно нравится, поэтому я надеялся на их шаблонное решение 😉

Ссылка: Play 2.0: Akka, Rest, Json и зависимости от нашего партнера JCG