您的位置:首页 > 其它

用Spray构建RESTful接口

2015-10-08 17:34 621 查看
可能很多人都在使用play2,因为play2就像Grails一样,直接download安装就可以用了,上手快,而且有Java版本。另外lift文档比较少,学习成本高,暂时不考虑使用lift(太难学)。Spray是个半成品,只包含RESTful,并且是基于akka,路由DSL设计,以及支持Servlet3.0实现。

基础文件配置

不多说,首先是构建SBT依赖:

import AssemblyKeys._

name := "rest"

version := "1.0"

scalaVersion := "2.10.5"

libraryDependencies ++= Seq(
"io.spray" % "spray-can" % "1.1-M8",
"io.spray" % "spray-http" % "1.1-M8",
"io.spray" % "spray-routing" % "1.1-M8",
"net.liftweb" %% "lift-json" % "2.5.1",
"com.typesafe.slick" %% "slick" % "1.0.1",
"mysql" % "mysql-connector-java" % "5.1.25",
"com.typesafe.akka" %% "akka-actor" % "2.1.4",
"com.typesafe.akka" %% "akka-slf4j" % "2.1.4",
"ch.qos.logback" % "logback-classic" % "1.0.13"
)

resolvers ++= Seq(
"Spray repository" at "http://repo.spray.io",
"Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
)

然后添加上相应的插件:

resolvers ++= Seq(
"Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/",
"Sonatype releases"  at "https://oss.sonatype.org/content/repositories/releases/"
)
addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.0")

配置文件application.conf添加上日志和数据源:

akka {
loglevel = DEBUG
event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
}
service {
host = "localhost"
port = 8080
}
db {
host = "localhost"
port = 3306
name = "rest"
user = "root"
password = null
}

lockback日志不多说了,自行在resources中添加即可。

引导配置

设置配置变量:

import com.typesafe.config.ConfigFactory
import util.Try

/**
* Holds service configuration settings.
*/
trait Configuration {

/**
* Application config object.
*/
val config = ConfigFactory.load()

/** Host name/address to start service on. */
lazy val serviceHost = Try(config.getString("service.host")).getOrElse("localhost")

/** Port to start service on. */
lazy val servicePort = Try(config.getInt("service.port")).getOrElse(8080)

/** Database host name/address. */
lazy val dbHost = Try(config.getString("db.host")).getOrElse("localhost")

/** Database host port number. */
lazy val dbPort = Try(config.getInt("db.port")).getOrElse(3306)

/** Service database name. */
lazy val dbName = Try(config.getString("db.name")).getOrElse("rest")

/** User name used to access database. */
lazy val dbUser = Try(config.getString("db.user")).toOption.orNull

/** Password for specified user and database. */
lazy val dbPassword = Try(config.getString("db.password")).toOption.orNull
}

客户端启动引导:

import akka.actor.{Props, ActorSystem}
import akka.io.IO
import com.madoka.example.config.Configuration
import com.madoka.example.rest.RestServiceActor
import spray.can.Http

object Boot extends App with Configuration {

// create an actor system for application
implicit val system = ActorSystem("rest-service-example")

// create and start rest service actor
val restService = system.actorOf(Props[RestServiceActor], "rest-endpoint")

// start HTTP server with rest service actor as a handler
IO(Http) ! Http.Bind(restService, serviceHost, servicePort)
}


领域模式构建

创建DAO层和实体:

/**
* Provides DAL for Customer entities for MySQL database.
*/
class CustomerDAO extends Configuration {

// init Database instance
private val db = Database.forURL(url = "jdbc:mysql://%s:%d/%s".format(dbHost, dbPort, dbName),
user = dbUser, password = dbPassword, driver = "com.mysql.jdbc.Driver")

// create tables if not exist
db.withSession {
if (MTable.getTables("customers").list().isEmpty) {
Customers.ddl.create
}
}

/**
* Saves customer entity into database.
*
* @param customer customer entity to
* @return saved customer entity
*/
def create(customer: Customer): Either[Failure, Customer] = {
try {
val id = db.withSession {
Customers returning Customers.id insert customer
}
Right(customer.copy(id = Some(id)))
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Updates customer entity with specified one.
*
* @param id       id of the customer to update.
* @param customer updated customer entity
* @return updated customer entity
*/
def update(id: Long, customer: Customer): Either[Failure, Customer] = {
try
db.withSession {
Customers.where(_.id === id) update customer.copy(id = Some(id)) match {
case 0 => Left(notFoundError(id))
case _ => Right(customer.copy(id = Some(id)))
}
}
catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Deletes customer from database.
*
* @param id id of the customer to delete
* @return deleted customer entity
*/
def delete(id: Long): Either[Failure, Customer] = {
try {
db.withTransaction {
val query = Customers.where(_.id === id)
val customers = query.run.asInstanceOf[Vector[Customer]]
customers.size match {
case 0 =>
Left(notFoundError(id))
case _ =>
query.delete
Right(customers.head)
}
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Retrieves specific customer from database.
*
* @param id id of the customer to retrieve
* @return customer entity with specified id
*/
def get(id: Long): Either[Failure, Customer] = {
try {
db.withSession {
Customers.findById(id).firstOption match {
case Some(customer: Customer) =>
Right(customer)
case _ =>
Left(notFoundError(id))
}
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Retrieves list of customers with specified parameters from database.
*
* @param params search parameters
* @return list of customers that match given parameters
*/
def search(params: CustomerSearchParameters): Either[Failure, List[Customer]] = {
implicit val typeMapper = Customers.dateTypeMapper

try {
db.withSession {
val query = for {
customer <- Customers if {
Seq(
params.firstName.map(customer.firstName is _),
params.lastName.map(customer.lastName is _),
params.birthday.map(customer.birthday is _)
).flatten match {
case Nil => ConstColumn.TRUE
case seq => seq.reduce(_ && _)
}
}
} yield customer

Right(query.run.toList)
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Produce database error description.
*
* @param e SQL Exception
* @return database error description
*/
protected def databaseError(e: SQLException) =
Failure("%d: %s".format(e.getErrorCode, e.getMessage), FailureType.DatabaseFailure)

/**
* Produce customer not found error description.
*
* @param customerId id of the customer
* @return not found error description
*/
protected def notFoundError(customerId: Long) =
Failure("Customer with id=%d does not exist".format(customerId), FailureType.NotFound)
}

创建实体模型:

import scala.slick.driver.MySQLDriver.simple._

/**
* Customer entity.
*
* @param id        unique id
* @param firstName first name
* @param lastName  last name
* @param birthday  date of birth
*/
case class Customer(id: Option[Long], firstName: String, lastName: String, birthday: Option[java.util.Date])

/**
* Mapped customers table object.
*/
object Customers extends Table[Customer]("customers") {

def id = column[Long]("id", O.PrimaryKey, O.AutoInc)

def firstName = column[String]("first_name")

def lastName = column[String]("last_name")

def birthday = column[java.util.Date]("birthday", O.Nullable)

def * = id.? ~ firstName ~ lastName ~ birthday.? <>(Customer, Customer.unapply _)

implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Date](
{
ud => new java.sql.Date(ud.getTime)
}, {
sd => new java.util.Date(sd.getTime)
})

val findById = for {
id <- Parameters[Long]
c <- this if c.id is id
} yield c
}

创建参数模型:

/**
* Customers search parameters.
*
* @param firstName first name
* @param lastName  last name
* @param birthday  date of birth
*/
case class CustomerSearchParameters(firstName: Option[String] = None,
lastName: Option[String] = None,
birthday: Option[Date] = None)


Actor模型

HTTP REST函数服务:

/**
* REST Service actor.
*/
class RestServiceActor extends Actor with RestService {

implicit def actorRefFactory = context

def receive = runRoute(rest)
}

/**
* REST Service
*/
trait RestService extends HttpService with SLF4JLogging {

val customerService = new CustomerDAO

implicit val executionContext = actorRefFactory.dispatcher

implicit val liftJsonFormats = new Formats {
val dateFormat = new DateFormat {
val sdf = new SimpleDateFormat("yyyy-MM-dd")

def parse(s: String): Option[Date] = try {
Some(sdf.parse(s))
} catch {
case e: Exception => None
}

def format(d: Date): String = sdf.format(d)
}
}

implicit val string2Date = new FromStringDeserializer[Date] {
def apply(value: String) = {
val sdf = new SimpleDateFormat("yyyy-MM-dd")
try Right(sdf.parse(value))
catch {
case e: ParseException =>
Left(MalformedContent("'%s' is not a valid Date value" format value, e))
}
}
}

implicit val customRejectionHandler = RejectionHandler {
case rejections => mapHttpResponse {
response =>
response.withEntity(HttpEntity(ContentType(MediaTypes.`application/json`),
write(Map("error" -> response.entity.asString))))
} {
RejectionHandler.Default(rejections)
}
}

val rest = respondWithMediaType(MediaTypes.`application/json`) {
path("customer") {
post {
entity(Unmarshaller(MediaTypes.`application/json`) {
case httpEntity: HttpEntity =>
read[Customer](httpEntity.asString(HttpCharsets.`UTF-8`))
}) {
customer: Customer =>
ctx: RequestContext =>
handleRequest(ctx, StatusCodes.Created) {
log.debug("Creating customer: %s".format(customer))
customerService.create(customer)
}
}
} ~
get {
parameters('firstName.as[String] ?, 'lastName.as[String] ?, 'birthday.as[Date] ?).as(CustomerSearchParameters) {
searchParameters: CustomerSearchParameters => {
ctx: RequestContext =>
handleRequest(ctx) {
log.debug("Searching for customers with parameters: %s".format(searchParameters))
customerService.search(searchParameters)
}
}
}
}
} ~
path("customer" / LongNumber) {
customerId =>
put {
entity(Unmarshaller(MediaTypes.`application/json`) {
case httpEntity: HttpEntity =>
read[Customer](httpEntity.asString(HttpCharsets.`UTF-8`))
}) {
customer: Customer =>
ctx: RequestContext =>
handleRequest(ctx) {
log.debug("Updating customer with id %d: %s".format(customerId, customer))
customerService.update(customerId, customer)
}
}
} ~
delete {
ctx: RequestContext =>
handleRequest(ctx) {
log.debug("Deleting customer with id %d".format(customerId))
customerService.delete(customerId)
}
} ~
get {
ctx: RequestContext =>
handleRequest(ctx) {
log.debug("Retrieving customer with id %d".format(customerId))
customerService.get(customerId)
}
}
}
}

/**
* Handles an incoming request and create valid response for it.
*
* @param ctx         request context
* @param successCode HTTP Status code for success
* @param action      action to perform
*/
protected def handleRequest(ctx: RequestContext, successCode: StatusCode = StatusCodes.OK)(action: => Either[Failure, _]) {
action match {
case Right(result: Object) =>
ctx.complete(successCode, write(result))
case Left(error: Failure) =>
ctx.complete(error.getStatusCode, net.liftweb.json.Serialization.write(Map("error" -> error.message)))
case _ =>
ctx.complete(StatusCodes.InternalServerError)
}
}
}

状态代码代数数据类型(ADT):

import spray.http.{StatusCodes, StatusCode}

/**
* Service failure description.
*
* @param message   error message
* @param errorType error type
*/
case class Failure(message: String, errorType: FailureType.Value) {

/**
* Return corresponding HTTP status code for failure specified type.
*
* @return HTTP status code value
*/
def getStatusCode: StatusCode = {
FailureType.withName(this.errorType.toString) match {
case FailureType.BadRequest => StatusCodes.BadRequest
case FailureType.NotFound => StatusCodes.NotFound
case FailureType.Duplicate => StatusCodes.Forbidden
case FailureType.DatabaseFailure => StatusCodes.InternalServerError
case _ => StatusCodes.InternalServerError
}
}
}

/**
* Allowed failure types.
*/
object FailureType extends Enumeration {
type Failure = Value

val BadRequest = Value("bad_request")
val NotFound = Value("not_found")
val Duplicate = Value("entity_exists")
val DatabaseFailure = Value("database_error")
val InternalError = Value("internal_error")
}


测试REST功能

启动Boot服务:

在目录结构中运行

sbt run

或者先构建

sbt assembly

再运行

java -jar <path-to-assembly.jar>

又或者直接通过IDE工具右键运行,成功后将在控制台出现

2015-10-08 17:20:41 INFO  [rest-service-example-akka.actor.default-dispatcher-4] a.e.s.Slf4jEventHandler - Slf4jEventHandler started
2015-10-08 17:20:42 DEBUG [rest-service-example-akka.actor.default-dispatcher-3] akka://rest-service-example/user/IO-HTTP/listener-0 - Binding to localhost/127.0.0.1:8080
2015-10-08 17:20:42 INFO  [rest-service-example-akka.actor.default-dispatcher-2] akka://rest-service-example/user/IO-HTTP/listener-0 - Bound to localhost/127.0.0.1:8080

如果使用集成开发工具,可以直接通过RESTClient测试



又或者通过CURL工具进行测试

POST方法

curl -v -X POST http://localhost:8080/customer -H "Content-Type: application/json" -d '{"firstName":
"First", "lastName":"Last", "birthday":"1990-01-01"}'




GET方法

curl -v -X GET http://localhost:8080/customer/1[/code] 


错误请求

curl -v -X GET http://localhost:8080/customer/1000[/code] 


由于PUT方法是等幂的,因此可以用于更新操作来处理表单的重复发送请求,但是不常用。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  spray slick akka actor