Статьи

Первые шаги с REST, Spray и Scala

На этом сайте вы уже можете найти пару статей о том, как сделать REST с несколькими различными фреймворками. Вы можете найти старый на Play , некоторые на Scalatra, и я даже начал (еще не законченный) сериал на Express . Так что вместо того, чтобы закончить серию на Express, я собираюсь посмотреть на Spray в этой статье.

Начиная

Первое, что нам нужно сделать, это настроить правильные библиотеки, чтобы мы могли начать разработку (я использую IntelliJ IDEA, но вы можете использовать все, что захотите). Самый простой способ начать работу — использовать SBT. Я использовал следующий минимальный файл SBT, чтобы начать.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
organization  := "org.smartjava"
  
version       := "0.1"
  
scalaVersion  := "2.11.2"
  
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
  
libraryDependencies ++= {
  val akkaV = "2.3.6"
  val sprayV = "1.3.2"
  Seq(
    "io.spray"            %%  "spray-can"     % sprayV withSources() withJavadoc(),
    "io.spray"            %%  "spray-routing" % sprayV withSources() withJavadoc(),
    "io.spray"            %%  "spray-json"    % "1.3.1",
    "io.spray"            %%  "spray-testkit" % sprayV  % "test",
    "com.typesafe.akka"   %%  "akka-actor"    % akkaV,
    "com.typesafe.akka"   %%  "akka-testkit"  % akkaV   % "test",
    "org.specs2"          %%  "specs2-core"   % "2.3.11" % "test",
    "org.scalaz"          %%  "scalaz-core"   % "7.1.0"
  )
}

После того, как вы импортировали этот файл в выбранную вами IDE, у вас должны быть правильные библиотеки spray и akka для начала.

Создать лаунчер

Далее давайте создадим панель запуска, которую вы сможете использовать для запуска нашего сервера Spray. Для этого мы просто объект с креативным именем Boot, который расширяет стандартную черту Scala App.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package org.smartjava;
  
import akka.actor.{ActorSystem, Props}
import akka.io.IO
import spray.can.Http
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
  
object Boot extends App {
  
  // create our actor system with the name smartjava
  implicit val system = ActorSystem("smartjava")
  val service = system.actorOf(Props[SJServiceActor], "sj-rest-service")
  
  // IO requires an implicit ActorSystem, and ? requires an implicit timeout
  // Bind HTTP to the specified service.
  implicit val timeout = Timeout(5.seconds)
  IO(Http) ? Http.Bind(service, interface = "localhost", port = 8080)
}

Там не так много происходит в этом объекте. Мы отправляем сообщение HTTP.Bind () (лучше сказать, что мы «просим»), чтобы зарегистрировать слушателя. Если привязка успешна, наш сервис будет получать сообщения всякий раз, когда на порт поступает запрос.

Прием актера

Теперь давайте посмотрим на актера, куда мы будем отправлять сообщения из подсистемы ввода-вывода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.smartjava
  
import akka.actor.Actor
import spray.routing._
import spray.http._
import MediaTypes._
import spray.httpx.SprayJsonSupport._
import MyJsonProtocol._
  
// simple actor that handles the routes.
class SJServiceActor extends Actor with HttpService {
  
  // required as implicit value for the HttpService
  // included from SJService
  def actorRefFactory = context
  
  // we don't create a receive function ourselve, but use
  // the runRoute function from the HttpService to create
  // one for us, based on the supplied routes.
  def receive = runRoute(aSimpleRoute ~ anotherRoute)
  
  // some sample routes
  val aSimpleRoute = {...}
  val anotherRoute = {...}

Так что здесь происходит то, что когда мы используем функцию runRoute, предоставляемую HttpService, для создания функции приема, которая обрабатывает входящие сообщения.

создание маршрутов

Последний шаг, который нам нужно настроить, — это создать некоторый код обработки маршрута. Более подробно об этой части мы расскажем в одной из следующих статей, поэтому покажем, как создать маршрут, который на основе входящего медиа-типа отправляет обратно некоторый JSON. Для этого мы будем использовать стандартную поддержку JSON от Spray. В качестве объекта JSON мы будем использовать следующий очень простой класс case, который мы расширили с помощью поддержки JSON.

1
2
3
4
5
6
7
8
9
package org.smartjava
  
import spray.json.DefaultJsonProtocol
  
object MyJsonProtocol extends DefaultJsonProtocol {
  implicit val personFormat = jsonFormat3(Person)
}
  
case class Person(name: String, fistName: String, age: Long)

Таким образом, Spray будет маршалировать этот объект в JSON, когда мы установим правильный медиа-тип ответа. Теперь, когда у нас есть наш объект ответа, давайте посмотрим на код для маршрутов:

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
// handles the api path, we could also define these in separate files
  // this path respons to get queries, and make a selection on the
  // media-type.
  val aSimpleRoute = {
    path("path1") {
      get {
  
        // Get the value of the content-header. Spray
        // provides multiple ways to do this.
        headerValue({
          case x@HttpHeaders.`Content-Type`(value) => Some(value)
          case default => None
        }) {
          // the header is passed in containing the content type
          // we match the header using a case statement, and depending
          // on the content type we return a specific object
          header => header match {
  
            // if we have this contentype we create a custom response
            case ContentType(MediaType("application/vnd.type.a"), _) => {
              respondWithMediaType(`application/json`) {
                complete {
                  Person("Bob", "Type A", System.currentTimeMillis());
                }
              }
            }
  
            // if we habe another content-type we return a different type.
            case ContentType(MediaType("application/vnd.type.b"), _) => {
              respondWithMediaType(`application/json`) {
                complete {
                  Person("Bob", "Type B", System.currentTimeMillis());
                }
              }
            }
  
            // if content-types do not match, return an error code
            case default => {
              complete {
                HttpResponse(406);
              }
            }
          }
        }
      }
    }
  }
  
  // handles the other path, we could also define these in separate files
  // This is just a simple route to explain the concept
  val anotherRoute = {
    path("path2") {
      get {
        // respond with text/html.
        respondWithMediaType(`text/html`) {
          complete {
            // respond with a set of HTML elements
            <html>
              <body>
                <h1>Path 2</h1>
              </body>
            </html>
          }
        }
      }
    }
  }

Там много кода, поэтому давайте выделим пару элементов в деталях:

1
2
3
4
5
val aSimpleRoute = {
    path("path1") {
      get {...}
   }
}

Эта начальная точка маршрута сначала проверяет, сделан ли запрос к пути «localhost: 8080 / path1», а затем проверяет метод HTTP. В этом случае нас интересуют только методы GET. Получив метод get, мы делаем следующее:

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
// Get the value of the content-header. Spray
        // provides multiple ways to do this.
        headerValue({
          case x@HttpHeaders.`Content-Type`(value) => Some(value)
          case default => None
        }) {
          // the header is passed in containing the content type
          // we match the header using a case statement, and depending
          // on the content type we return a specific object
          header => header match {
  
            // if we have this contentype we create a custom response
            case ContentType(MediaType("application/vnd.type.a"), _) => {
              respondWithMediaType(`application/json`) {
                complete {
                  Person("Bob", "Type A", System.currentTimeMillis());
                }
              }
            }
  
            // if we habe another content-type we return a different type.
            case ContentType(MediaType("application/vnd.type.b"), _) => {
              respondWithMediaType(`application/json`) {
                complete {
                  Person("Bob", "Type B", System.currentTimeMillis());
                }
              }
            }
  
            // if content-types do not match, return an error code
            case default => {
              complete {
                HttpResponse(406);
              }
            }
          }
        }
      }

В этом фрагменте кода мы извлекаем заголовок Content-Type запроса и на основании этого определяем ответ. Ответ автоматически преобразуется в JSON, потому что responseWithMediaType установлен в application / json. Если указан медиатип, который мы не понимаем, мы возвращаем сообщение 406.

Давайте проверим это

Теперь давайте проверим, работает ли это. Spray предоставляет собственные библиотеки и классы для тестирования, но сейчас давайте просто воспользуемся простым базовым клиентом отдыха. Для этого я обычно использую Chrome Advanced Rest Client . На следующих двух снимках экрана вы можете видеть три звонка на http: // localhost: 8080 / path1:

Вызов с медиа-типом «application / vnd.type.a»:

Снимок экрана 2014-11-11 в 11.13.10

Вызов с медиа-типом «application / vnd.type.b»:

Снимок экрана 2014-11-11 в 11.13.31

Вызов с медиа-типом «application / vnd.type.c»:

Снимок экрана 2014-11-11 в 11.13.47

Как видите, ответы точно соответствуют маршрутам, которые мы определили.

Что дальше

В следующей статье мы подключим Spray IO к базе данных, немного упростим тестирование и рассмотрим ряд других функций Spray.IO.

Ссылка: Первые шаги с REST, Spray и Scala от нашего партнера JCG Йоса Дирксена в блоге Smart Java .