scala入门
在本教程的第二部分中,您将学习如何执行以下操作:
- 如何使用嵌入式Jetty启动scalatra以进行简单的测试和调试
- 创建一个简单的REST API以返回JSON数据
- 使用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。 目前,我们不会做得太大,只能提供三种操作:
- 根据编号获取拍卖品。
- 对特定项目出价。
- 获得用户的出价。
我们还不会添加持久性(这是下一个教程的内容),我们只会在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入门