Статьи

Начало работы со Scala и Scalatra — Часть II

В предыдущей части руководства мы создали простое приложение с нуля и настроили Eclipse, чтобы мы могли редактировать файлы scala для scalatra.

Во второй части урока вы узнаете, как сделать следующее:

  1. Как запустить scalatra со встроенным Jetty для легкого тестирования и отладки
  2. Создайте простой REST API, который возвращает данные JSON
  3. Протестируйте свой сервис, используя specs2

Мы начнем с изменения способа, которым мы начинаем скалатру. Вместо того, чтобы запускать его с использованием sbt, как мы делали в предыдущей части, мы начнем скалатру непосредственно из Eclipse. Вы можете скачать проект для этого урока здесь. Не забудьте запустить следующее из каталога проекта перед импортом в Eclipse.

1
2
3
$ sbt
> update
> eclipse

Как запустить scalatra со встроенным Jetty для легкого тестирования и отладки

Мы видели, что вы можете запустить scalatra (и ваш сервис) напрямую, используя sbt.

1
2
3
$ sbt
> container:start
> ~ ;copy-resources;aux-compile

Это запустит сервер Jetty, автоматически скопирует ресурсы и скомпилирует их. Даже при том, что это работает нормально, иногда вы можете столкнуться с проблемами с памятью, когда перезагрузка останавливается, отладка очень трудна, и когда выдается исключение, вы не можете просто нажать на исключение, чтобы перейти к соответствующему исходному коду. Это то, что мы можем легко исправить. Скалатра использует Jetty для внутреннего использования и сама является не чем иным, как сервлетом. Так что мы можем просто запустить встроенный экземпляр Jetty, который указывает на сервлет. Для этого мы создаем следующий объект scala.

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
package org.smartjava.scalatra.server;
  
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.webapp.WebAppContext
  
object JettyEmbedded {
  
  def main(args: Array[String]) {
    val server = new Server(9080)
    val context: WebAppContext = new WebAppContext();
    context.setServer(server)
    context.setContextPath('/');
    context.setWar('src/main/webapp')
    server.setHandler(context);
  
    try {
      server.start()
      server.join()
    } catch {
      case e: Exception => {
        e.printStackTrace()
        System.exit(1)
      }
    }
  }
}

Перед выполнением этого также создайте файл logback.xml, чтобы контролировать ведение журнала. Это просто базовая конфигурация регистрации, которая регистрирует сообщение только на информационном уровне или выше. Если у вас этого нет, вы увидите множество сообщений журнала Jetty. Для наших собственных сообщений журнала мы устанавливаем уровень для отладки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<configuration>
  <appender name='STDOUT' class='ch.qos.logback.core.ConsoleAppender'>
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  
  <logger name='org.smartjava.scalatra' level='DEBUG'/>
  
  <root level='info'>
    <appender-ref ref='STDOUT' />
  </root>
</configuration>

Вы можете запустить его как приложение непосредственно из Eclipse: «Выполнить-> Выполнить как-> Приложение Scala». Вывод, который вы увидите, будет примерно таким:

1
2
3
4
5
6
7
8
21:37:33.421 [main] INFO  org.eclipse.jetty.server.Server - jetty-8.1.5.v20120716
21:37:33.523 [main] INFO  o.e.j.w.StandardDescriptorProcessor - NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
21:37:33.589 [main] INFO  o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.590 [main] INFO  o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.631 [main] INFO  o.scalatra.servlet.ScalatraListener - Initializing life cycle class: Scalatra
21:37:33.704 [main] INFO  o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.791 [main] INFO  o.f.s.servlet.ServletTemplateEngine - Scalate template engine using working directory: /var/folders/mc/vvzshptn22lg5zpp7fdccdzr0000gn/T/scalate-6431313014401266228-workdir
21:37:33.812 [main] INFO  o.e.jetty.server.AbstractConnector - Started [email protected]:9080

Теперь вы можете работать со скалатрами прямо из Eclipse. Ну, это было легко. Следующий шаг, давайте создадим API REST. Однако прежде чем мы сделаем это, если вы работаете с Chrome, установите Dev HTTP Client . Это отличный HTTP-клиент, который работает прямо из Chrome. Очень прост в использовании.

Создайте простой REST API, который возвращает данные JSON

В этой части руководства мы начнем с создания очень простого REST API. Мы создадим API, который позволит нам делать ставки на товары. Этакий мини eBay. В настоящее время мы не будем делать его очень большим и будем выполнять только три операции:

  1. Получить аукцион на основе идентификатора.
  2. Сделайте ставку на конкретный товар.
  3. Получить ставку, которую сделал пользователь.

Мы пока не добавим постоянство (это что-то для следующего урока), мы только рассмотрим аспекты API. Начнем с первого.

Получить аукцион на основе идентификатора

Для этого мы хотим сделать следующее:

Запрос:

1
GET /items/123

Отклик:

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
200 OK
Content-Length: 434
Server: Jetty(8.1.5.v20120716)
Content-Type: application/vnd.smartbid.item+json;charset=UTF-8
  
{
 'name':'Monty Python and the search for the holy grail',
 'id':123,
 'startPrice':0.69,
 'currency':'GBP',
 'description':'Must have item',
 'links':[
   {
    'linkType':'application/vnd.smartbid.item',
    'rel':'Add item to watchlist',
    'href':'/users/123/watchlist'
   },
   {
    'linkType':'application/vnd.smartbid.bid',
    'rel':'Place bid on item',
    'href':'/items/123/bid'
   },
   {
    'linkType':'application/vnd.smartbid.user',
    'rel':'Get owner's details',
    'href':'/users/123'
   }
 ]
}

Как вы можете видеть, мы делаем простой запрос GET к определенному URL, и мы получаем детали элемента. Этот пункт имеет некоторые свойства и ряд ссылок. Эти ссылки могут быть использованы пользователем вашего API для изучения других ресурсов или выполнения каких-либо действий в вашем API. Я не буду вдаваться в подробности, но если вы хотите знать, что нужно сделать, чтобы создать простой в использовании и гибкий API, посмотрите на мою презентацию .

Мы знаем, что нужно сделать нашему клиенту, чтобы получить этот ресурс. Код скалатры очень прост:

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
package org.smartjava.scalatra
  
import grizzled.slf4j.Logger
import org.scalatra._
import scalate.ScalateSupport
import net.liftweb.json.compact
import net.liftweb.json.render
import net.liftweb.json.JsonDSL._
import net.liftweb.json.Serialization.{read, write}
import org.smartjava.scalatra.repository.ItemRepository
import net.liftweb.json.Serialization
import net.liftweb.json.NoTypeHints
import org.scalatra.Ok
import org.scalatra.NotFound
import org.smartjava.scalatra.repository.BidRepository
import org.scalatra.Created
import scala.collection.immutable.Map
import org.smartjava.scalatra.model.Bid
  
class HelloScalatraServlet extends ScalatraServlet with ScalateSupport {
  
  // simple logger
  val logger = Logger(classOf[HelloScalatraServlet]);
  
  // repo stores our items
  val itemRepo = new ItemRepository;
  val bidRepo = new BidRepository;
  
  // implicit value for json serialization format
  implicit val formats = Serialization.formats(NoTypeHints);
  
  get('/items/:id') {
    // set the result content type
    contentType = 'application/vnd.smartbid.item+json'
  
    // convert response to json and return as OK
    itemRepo.get(params('id').toInt) match {
      case Some(x) => Ok(write(x));
      case None => NotFound('Item with id ' + params('id') + ' not found');
    }
  }
}

Для этой первой операции REST я перечислю полный класс, для остальных я покажу только соответствующие функции. Для обработки запроса нам нужно определить «маршрут».

1
2
3
4
5
6
7
8
9
get('/items/:id') {
  // set the result content type
  contentType = 'application/vnd.smartbid.item+json'
 
  // convert response to json and return as OK
  itemRepo.get(params('id').toInt) match {
    case Some(x) => Ok(write(x));
    case None => NotFound('Item with id ' + params('id') + ' not found');
  }

Этот маршрут прослушивает операции GET для / items /: id url. Всякий раз, когда запрос получен, эта функция вызывается. В этой функции мы сначала устанавливаем результирующий тип контента. Я являюсь сторонником создания пользовательских медиа-типов для своих ресурсов, поэтому мы установили наш тип содержимого результата ‘application / vnd.smartbid.item + json’. Затем нам нужно извлечь наш элемент из нашего хранилища и сериализовать его в JSON.

Для сериализации JSON я использовал lift-json. С помощью этой библиотеки вы можете автоматически сериализовать классы дел (или создавать и анализировать json вручную). Чтобы использовать lift-json, вам нужно добавить следующую строку в libraryDependencies в файле build.sbt и обновить проект eclipse из sbt.

1
'net.liftweb' %% 'lift-json' % '2.4',

Код, который записывает наши файлы классов как json, — это одна строка

1
case Some(x) => Ok(write(x));

Если мы можем найти элемент в хранилище, мы записываем его как json, используя функцию записи. Мы возвращаем этот JSON как ответ «200 OK», используя функцию «скалатра OK». Если ресурс не может быть найден, мы отправили 404, используя эту строку.

1
case None => NotFound('Item with id ' + params('id') + ' not found');

Для полноты картины я перечислю реализацию модели и фиктивного репо:

Модель:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case class Item(
    name:String,
    id: Number,
    startPrice: Number,
    currency: String,
    description: String,
    links: List[Link]
);
  
case class Link(
    linkType: String,
    rel: String,
    href: String
);
  
case class Bid(
    id: Option[Long],
    forItem: Number,
    minimum: Number,
    maximum: Number,
    currency: String,
    bidder: String,
    date: Long
);

Пустышка репо:

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
  class ItemRepository {
    def get(id: Number) : Option[Item] = {
  
      id.intValue() match {
        case 123 => {
           val l1 = new Link('application/vnd.smartbid.item','Add item to watchlist','/users/123/watchlist');
           val l2 = new Link('application/vnd.smartbid.bid','Place bid on item','/items/' + id + '/bid');
           val l3 = new Link('application/vnd.smartbid.user','Get owner's details','/users/123');
  
           val item = new Item(
             'Monty Python and the search for the holy grail',
             id,
             0.69,
             'GBP',
             'Must have item',
             List(l1,l2,l3));
  
           Option(item);
        };
  
        case _ => Option(null);
      }
    }
  
    def delete(item: Item) = println('deleting user: ' + item)
}

С этим кодом мы завершили нашу первую операцию REST. Мы можем легко протестировать этот сервис, используя клиент Chrome Dev HTTP, о котором я упоминал ранее

В ответе вы видите несколько ссылок, одна из которых следующая:

1
2
3
4
5
{
  'linkType':'application/vnd.smartbid.bid',
  'rel':'Place bid on item',
  'href':'/items/123/bid'
 }

Вы можете увидеть атрибут href здесь. Мы можем перейти по этой ссылке, чтобы сделать ставку.

Сделайте ставку на конкретный товар.

Для этого нам нужно сделать POST для «/ items / 123 / bid» со ставкой типа «application / vnd.smartbid.bid». Формат выглядит так:

1
2
3
4
5
6
7
8
{
'forItem':123,
'minimum':20,
'maximum':10,
'currency':'GBP',
'bidder':'jdirksen',
'date':1347269593301
}

Давайте еще раз посмотрим на код этой операции.

1
2
3
4
5
post('/items/:id/bid', request.getContentType == 'application/vnd.smartbid.bid+json') {
   contentType = 'application/vnd.smartbid.bid+json'
var createdBid = bidRepo.create(read[Bid](request.body));
Created(write(createdBid), Map('Location'->('/users/' + createdBid.bidder + '/bids/'+createdBid.id.get)));
 }

Как видно по названию, эта операция прослушивает POST для «/ items /: id / bid». Поскольку я хочу, чтобы API управлялся медиа-типом, я добавил дополнительное условие к этому маршруту. С помощью ‘request.getContentType ==’ application / vnd.smartbid.bid + json ‘мы требуем, чтобы клиенты этой операции указывали, что тип отправленного ими ресурса относится к этому конкретному типу.
Сама операция не так сложна. Мы устанавливаем тип содержимого для результата и используем репозиторий для создания ставки. Для этого мы используем операцию чтения из lift-json для преобразования входящего JSON в объект scala. Созданный объект возвращается с сообщением о состоянии «201 Created» и содержит заголовок местоположения, который указывает на ресурс, который мы только что создали.

Получить ставку, которую сделал пользователь.

Последняя операция, которую мы сейчас поддерживаем, — это простая операция, в которой мы можем просмотреть только что созданную ставку. Мы знаем, где искать, потому что местоположение только что созданного ресурса было возвращено в заголовке местоположения. Скала-код для этой функции показан здесь:

01
02
03
04
05
06
07
08
09
10
/**
 * Route that matches retrieval of bids
 */
get('/users/:user/bids/:bid') {
  contentType = 'application/vnd.smartbid.bid+json'
  bidRepo.get(params('bid').toInt,params('user')) match {
    case Some(x) => Ok(write(x));
    case None => NotFound('Bid with id ' + params('bid') + ' not found for user: ' + params('user') );
  }
}

Практически так же, как мы видели для поиска предметов. Мы извлекаем ресурс из репозитория, если он существует, мы возвращаем его с «200 OK», если нет, мы возвращаем 404.

Протестируйте свой сервис, используя specs2

В последнем разделе этой части руководства мы кратко рассмотрим тестирование. Когда вы создаете новый проект Scalatra, как мы показали в предыдущей части, мы также получаем тест-заглушку, который мы можем расширить для тестирования нашего сервиса. В этом примере я не буду писать простые тесты JUnit низкого уровня, но мы создадим спецификацию, которая описывает, как должен работать наш API. Код для (части) спецификации приведен здесь:

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
package org.smartjava.scalatra
  
import org.scalatra.test.specs2._
import org.junit.runner.RunWith
import org.scalatra.test.Client
import org.specs2.SpecificationWithJUnit
import org.eclipse.jetty.util.log.Log
  
/**
 * Set of JUnit test to test our API
 */
class HelloScalatraServletSpec extends ScalatraSpec {
  
    // add the servlet so we can start testing                                                     
  addServlet(classOf[HelloScalatraServlet], '/*')
  
  // some constants
  val EXPECTED_BID = '''{'id':345,'forItem':123,'minimum':20,'maximum':10,'currency':'GBP','bidder':'jdirksen','date':1347285103671}'''
  val BID_URL = '/users/jdirksen/bids/345';
  val MED_TYPE = 'application/vnd.smartbid.bid+json'
  
  
  def is =
    'Calling an unknown url on the API '            ^
      'returns status 404'                           ! statusResult('/unknown',404)^
                end ^ p ^
    'Calling a GET on ' + BID_URL + ' should'       ^
    'return status 200'                             ! statusResult(BID_URL,200)^
    'and body should equal: ' + EXPECTED_BID        ! {get(BID_URL){response.body must_== EXPECTED_BID}}^
    'and media-type should equal: ' + MED_TYPE  ! {get(BID_URL){response.getContentType must startWith(MED_TYPE)}}
                end
  
  def statusResult(url:String,code:Int) =
    get(url) {
   status must_== code
  }
}

Для более полного ознакомления с specs2 посмотрите их веб-сайт , я просто объясню код, показанный здесь. В этом коде мы создаем сценарий части «def is». «is» содержит ряд утверждений, которые должны быть истинными.

1
2
3
'Calling an unknown url on the API '            ^
   'returns status 404'                           ! statusResult('/unknown',404)^
             end ^ p ^

Первый тест, который мы делаем, проверяет, что происходит, когда мы вызываем неизвестный URL в нашем API. Мы определяем, что для этого мы ожидаем 404. Мы проверяем это, вызывая функцию statusResult. Если возвращается 404, эта проверка пройдет, в противном случае мы увидим это в результатах. Фактическая функция statusResult также определена в этом файле. Эта функция использует встроенную функцию get для вызова нашего API, который запускается встроенным из этого теста.

Далее мы собираемся проверить, как должен работать URL для получения ставки.

1
2
3
4
'Calling a GET on ' + BID_URL + ' should'       ^
 'return status 200'                             ! statusResult(BID_URL,200)^
 'and body should equal: ' + EXPECTED_BID        ! {get(BID_URL){response.body must_== EXPECTED_BID}}^
 'and media-type should equal: ' + MED_TYPE  ! {get(BID_URL){response.getContentType must startWith(MED_TYPE)}}

Как видите, такая же базовая структура соблюдается. Мы проводим ряд проверок, которые должны пройти. Если мы запустим это, мы сразу увидим, как должен вести себя наш API (мгновенная документация) и подтверждает ли это нашу спецификацию. Это результат теста.

01
02
03
04
05
06
07
08
09
10
11
12
13
HelloScalatraServletSpec
  
Calling an unknown url on the API
+ returns status 404
  
Calling a GET on /users/jdirksen/bids/345 should
+ return status 200
+ and body should equal: {'id':345,'forItem':123,'minimum':20,'maximum':10,'currency':'GBP','bidder':'jdirksen','date':1347285103671}
+ and media-type should equal: application/vnd.smartbid.bid+json
  
Total for specification HelloScalatraServletSpec
Finished in 846 ms
4 examples, 0 failure, 0 error

Specs2 имеет несколько различных способов его запуска. Он может быть запущен напрямую как тестовый сценарий JUnit, из maven или с помощью собственного запуска. Поскольку я работаю в Eclipse, я хотел запустить эти тесты непосредственно из Eclipse. Итак, я начал с тестера JUnit. Проблема, однако, с этим бегуном в том, что он, кажется, конфликтует с внутренне используемой Jetty из Eclipse. Когда я разглагольствовал это, тест пытался связаться с экземпляром Jetty через порт 80, вместо использования встроенного, который он запустил сам. Чтобы это исправить, я создал простой лаунчер, который запускал этот тест напрямую. Для этого выполните следующую конфигурацию запуска, чтобы получить вывод, который я только что показал.

Запустите конфигурацию часть 1

Запустите конфигурацию часть 2

Теперь, когда вы запускаете эту конфигурацию, запускаются тесты specs2.

Вот именно для этой части урока. В следующей части мы рассмотрим доступ к базе данных и использование akka.

Приятного кодирования и не забудьте поделиться!

Ссылка: Учебное пособие: Начало работы со scala и scalatra — часть II от нашего партнера по JCG Йоса Дирксена из блога Smart Java .