可能很多人都在使用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)
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") }
启动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
错误请求
curl -v -X GET http://localhost:8080/customer/1000
由于PUT方法是等幂的,因此可以用于更新操作来处理表单的重复发送请求,但是不常用。