Ребята из 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 )} |
Как использовать этот файл довольно просто, как только вы прочитаете документацию 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 )} |
С настроенными зависимостями я могу настроить этот проект для своей 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): FirstStepsWithPlay2jos@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.processGetAllRequestGET /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.GeoJsonServiceimport play.api.mvc.Actionimport 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.actorRef2Scalaimport akka.actor.Actorimport akka.actor.Propsimport akka.dispatch.Awaitimport akka.pattern.askimport akka.util.duration.intToDurationIntimport akka.util.Timeoutimport model.GeoRecordimport play.libs.Akkaimport resources.commands.Commandimport resources.commands.FULLimport resources.commands.SINGLEimport 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