scala入门_Scala和Scalatra入门–第二部分

scala入门

在本教程上一部分中,我们从头开始创建了一个简单的应用程序,并设置了Eclipse,以便我们可以编辑scalatra的scala文件。

在本教程的第二部分中,您将学习如何执行以下操作:

  1. 如何使用嵌入式Jetty启动scalatra以进行简单的测试和调试
  2. 创建一个简单的REST API以返回JSON数据
  3. 使用specs2测试您的服务

我们将从更改scalatra的启动方式开始。 与其像上一部分中那样使用sbt运行它,我们将直接从Eclipse启动scalatra。 您可以从此处下载本教程的项目 在导入Eclipse之前,请记住从项目目录运行以下命令。

$ sbt
> update
> eclipse

如何使用嵌入式Jetty启动scalatra以进行简单的测试和调试

我们已经看到您可以直接使用sbt启动scalatra(和您的服务)。

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

这将启动Jetty服务器并自动复制资源并进行编译。 即使这种方法行之有效,您有时仍会遇到重新加载停止的内存问题,调试非常困难,并且当引发异常时,您不能仅单击该异常以跳至相关的源代码。 这是我们可以轻松解决的问题。 Scalatra在内部使用Jetty,它本身仅是一个servlet。 因此,我们可以做的就是运行一个指向servlet的嵌入式Jetty实例。 为此,我们创建以下scala对象。

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日志消息。 对于我们自己的日志消息,我们将级别设置为调试。

<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将其作为应用程序运行:“运行->运行方式->标量应用程序”。 您将看到的输出将是这样的:

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 SelectChannelConnector@0.0.0.0:9080

现在,您可以直接从Eclipse使用scalatra。 好吧,这很容易。 下一步,让我们创建一个REST API。 不过,在执行此操作之前,如果您使用的是Chrome,请安装“ Dev HTTP Client ”。 这是一个很棒的HTTP客户端,可以直接在Chrome浏览器中运行。 很好用。

创建一个简单的REST API以返回JSON数据

在本部分教程中,我们将从创建一个非常简单的REST API开始。 我们将创建一个API,允许我们对商品出价。 一种迷你eBay。 目前,我们不会做得太大,只能提供三种操作:

  1. 根据编号获取拍卖品。
  2. 对特定项目出价。
  3. 获得用户的出价。

我们还不会添加持久性(这是下一个教程的内容),我们只会在API方面着眼。 我们将从第一个开始。

根据编号获取拍卖品

为此,我们希望能够执行以下操作:

请求:

GET /items/123

响应:

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'
   }
 ]
}

如您所见,我们对特定的URL进行了简单的GET请求,然后返回的是项目的详细信息。 此项具有一些属性和许多链接。 API用户可以跟随这些链接来浏览其他资源或对API执行某些操作。 在这里,我将不做详细介绍,但是如果您想知道如何创建易于使用且灵活的API,请查看我的演示文稿

我们知道客户需要做些什么才能获得此资源。 标量代码非常简单:

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操作,我列出了完整的类,其余的我仅显示相关的功能。 为了处理请求,我们需要定义一个“路线”。

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');
    }

该路由在/ items /:id网址上侦听GET操作。 每当收到请求时,都会调用此函数。 在此函数中,我们首先设置结果内容类型。 我支持为资源创建自定义媒体类型,因此我们将结果内容类型设置为“ application / vnd.smartbid.item + json”。 接下来,我们需要从存储库中检索项目并将其序列化为JSON。

对于JSON序列化,我使用了lift-json。 使用此库,您可以自动序列化案例类(或手动创建和解析json)。 要使用lift-json,您需要将以下行添加到build.sbt文件中的libraryDependencies并从sbt更新eclipse项目。

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

将我们的类文件写为json的代码是这种单行代码

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

如果我们可以在存储库中找到该项目,则可以使用write函数将其写为json。 我们使用scalatra OK函数将此JSON作为“ 200 OK”响应返回。 如果找不到资源,则使用此单线发送404。

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

为了完整起见,我将列出模型和虚拟仓库实现:

模型:

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
);

虚拟回购:

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客户端轻松测试此服务:

在响应中,您可以看到许多链接,其中一个是以下链接:

{
    'linkType':'application/vnd.smartbid.bid', 
    'rel':'Place bid on item',
    'href':'/items/123/bid'
   }

您可以在此处看到href属性。 我们可以点击此链接进行出价。

对特定项目出价。

为此,我们需要对“ / items / 123 / bid”进行POST,出价类型为“ application / vnd.smartbid.bid”。 格式如下:

{
    'forItem':123,
    'minimum':20,
    'maximum':10,
    'currency':'GBP',
    'bidder':'jdirksen',
    'date':1347269593301
    }

让我们再次看一下该操作的代码。

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)));
  }

如您所见,此操作在'/ items /:id / bid'上侦听POST。 因为我希望API是媒体类型驱动的,所以我为此路由添加了额外的条件。 使用'request.getContentType =='application / vnd.smartbid.bid + json'时,我们要求此操作的客户端指示其发送的资源类型属于此特定类型。
操作本身并不复杂。 我们设置结果的内容类型,并使用存储库创建出价。 为此,我们使用了来自lift-json的读取操作将传入的JSON转换为scala对象。 创建的对象将返回“ 201 Created”状态消息,并包含一个位置标头,该标头指向我们刚刚创建的资源。

获得用户的出价。

现在,我们支持的最终操作非常简单,我们可以在其中查看刚刚创建的出价。 我们知道在哪里看,因为刚刚创建的资源的位置是在location标头中返回的。 此函数的scala代码如下所示:

/**
   * 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如何工作的规范。 规范(一部分)的代码在此处列出:

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”部分。 “是”包含许多必须为真的语句。

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

我们做的第一个测试是检查在API上调用未知URL时会发生什么。 我们为此定义了一个404。我们通过调用statusResult函数进行检查。 如果返回404,则此检查将通过,否则,我们将在结果中看到此检查。 实际的“ statusResult”功能也在此文件中定义。 此函数使用内置的“ get”函数来调用我们的API,该API在此测试中运行。

接下来,我们将检查获取出价网址的工作方式。

'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的行为方式(即时文档)以及它是否符合我们的规范。 这是测试的输出。

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具有多种不同的运行方式。 它可以直接从Maven作为JUnit测试用例运行,也可以使用自己的启动器运行。 由于我在Eclipse中进行开发,因此我想直接从Eclipse运行这些测试。 因此,我从JUnit testrunner开始。 但是,该运行程序的问题在于,它似乎与Eclipse中内部使用的Jetty冲突。 当我对此进行测试时,该测试尝试与端口80上的一个Jetty实例联系,而不是使用它自己启动的嵌入式实例。 为了解决这个问题,我创建了一个简单的启动器,可以直接运行此测试。 为此,请执行以下启动配置以获取我刚刚显示的输出。

运行配置第1部分

运行配置第2部分

现在,无论何时运行此配置,都会运行specs2测试。

在本部分教程中就是这样。 在下一部分中,我们将研究数据库访问和使用akka。

祝您编程愉快,别忘了分享!

参考: 教程:scala和scalatra入门–第二部分,来自我们的JCG合作伙伴 Jos Dirksen,来自Smart Java博客。


翻译自: https://www.javacodegeeks.com/2012/09/getting-started-with-scala-and-scalatra_12.html

scala入门

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值