2019独角兽企业重金招聘Python工程师标准>>>
可能很多人都在使用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
又或者直接通过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
错误请求
curl -v -X GET http://localhost:8080/customer/1000
由于PUT方法是等幂的,因此可以用于更新操作来处理表单的重复发送请求,但是不常用。