Во второй части урока вы узнаете, как сделать следующее:
- Как запустить scalatra со встроенным Jetty для легкого тестирования и отладки
- Создайте простой REST API, который возвращает данные JSON
- Протестируйте свой сервис, используя 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. В настоящее время мы не будем делать его очень большим и будем выполнять только три операции:
- Получить аукцион на основе идентификатора.
- Сделайте ставку на конкретный товар.
- Получить ставку, которую сделал пользователь.
Мы пока не добавим постоянство (это что-то для следующего урока), мы только рассмотрим аспекты 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.slf 4 j.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 l 1 = new Link( 'application/vnd.smartbid.item' , 'Add item to watchlist' , '/users/123/watchlist' ); val l 2 = new Link( 'application/vnd.smartbid.bid' , 'Place bid on item' , '/items/' + id + '/bid' ); val l 3 = 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.specs 2 . _ import org.junit.runner.RunWith import org.scalatra.test.Client import org.specs 2 .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 .