Facebook Twitter LinkedIn E-mail
magnify
Home 2014 八月

Play Framework Web开发教程(24): 绑定HTTP数据到Scala对象

上篇介绍了路由配置,如果将HTTP请求映射到Controller的Action方法,下一步是如何解析参数比如EAN代码5010255079763.HTTP请求本身没有定义数据类型,因此所有的HTTP数据实际上都是字符串(或者是文本类型),也就是我们需要把这个13位的字符串转换成数字。
Play框架,能够在调用Action方法之前,将字符串转换成所需要的数据类型,也只有在数据转换成功后,Play才会调用对应的Action方法。为了能够在路由器调用Action方法之前执行参数类型转换,路由器首先使用正确的Scala类型构造一个对象作为参数传入Action方法,这个过程称为“绑定”。它由多种类型相关的绑定器(binder)解析HTTP文本数据来实现,如下图所示:

201400831001

在这个图中,我们可以看到路由的过程,包含数据绑定。 下面为路由器处理PUT /product/5010255079763 所发生的步骤:

  1. 路由器根据路由配置将HTTP请求匹配到controllers.Products.update(ean: Long)。
  2. 路由器使用类型相关的绑定器,本例为Long绑定器解析输入字符串5010255079763为Scala的Long对象。
  3. 路由器调用选定的路由指定的Action方法Products.update ,传入5010255079763L 作为参数。

 
绑定的特别之处意味着Play支持参数类型安全。比如,你可以把下面两个URL根据参数类型不同映射到不同action方法:
/product/5010255079763
/product/paper-clips-large-plain-1000

数据绑定支持两种类型的请求数据,一个URL中的参数,另外是POST请求中的查询参数,在Controller层面来说,两种数据绑定的方法是一样的,也就是对于Action方法参数来说,无需关心参数究竟是使用何种方法请求的。

如果输入的参数类型不能转换成所定义的数据类型,比如你发生一个请求/product/x ,x无法转换成整数,此时Play会返回一个400(Bad Request)和一个错误页面:

201400831002

 

Akka 编程(20):容错处理(一)

我们在前面介绍Actor系统时说过每个Actor都是其子Actor的管理员,并且每个Actor定义了发生错误时的管理策略,策略一旦定义好,之后不能修改,就像是Actor系统不可分割的一部分。
实用错误处理
首先我们来看一个例子来显示一种处理数据存储错误的情况,这是现实中一个应用可能出现的典型错误。当然实际的应用可能针对数据源不存在时有不同的处理,这里我们使用重新连接的处理方法。
下面是例子的源码,比较长,需要仔细阅读,最好是实际运行,参考日志来理解:

import akka.actor._
import akka.actor.SupervisorStrategy._
import scala.concurrent.duration._
import akka.util.Timeout
import akka.event.LoggingReceive
import akka.pattern.{ask, pipe}
import com.typesafe.config.ConfigFactory

/**
 * Runs the sample
 */
object FaultHandlingDocSample extends App {

  import Worker._

  val config = ConfigFactory.parseString( """
      akka.loglevel = "DEBUG"
      akka.actor.debug {
      receive = on
      lifecycle = on
      }
      """)

  val system = ActorSystem("FaultToleranceSample", config)
  val worker = system.actorOf(Props[Worker], name = "worker")
  val listener = system.actorOf(Props[Listener], name = "listener")
  // start the work and listen on progress
  // note that the listener is used as sender of the tell,
  // i.e. it will receive replies from the worker
  worker.tell(Start, sender = listener)
}

/**
 * Listens on progress from the worker and shuts down the system when enough
 * work has been done.
 */
class Listener extends Actor with ActorLogging {

  import Worker._

  // If we don’t get any progress within 15 seconds then the service is unavailable
  context.setReceiveTimeout(15 seconds)

  def receive = {
    case Progress(percent) =>
      log.info("Current progress: {} %", percent)
      if (percent >= 100.0) {
        log.info("That’s all, shutting down")
        context.system.shutdown()
      }
    case ReceiveTimeout =>
      // No progress within 15 seconds, ServiceUnavailable
      log.error("Shutting down due to unavailable service")
      context.system.shutdown()
  }
}

object Worker {

  case object Start

  case object Do

  final case class Progress(percent: Double)

}

/**
 * Worker performs some work when it receives the ‘Start‘ message.
 * It will continuously notify the sender of the ‘Start‘ message
 * of current ‘‘Progress‘‘. The ‘Worker‘ supervise the ‘CounterService‘.
 */
class Worker extends Actor with ActorLogging {

  import Worker._
  import CounterService._

  implicit val askTimeout = Timeout(5 seconds)
  // Stop the CounterService child if it throws ServiceUnavailable
  override val supervisorStrategy = OneForOneStrategy() {
    case _: CounterService.ServiceUnavailable => Stop
  }
  // The sender of the initial Start message will continuously be notified
  // about progress
  var progressListener: Option[ActorRef] = None
  val counterService = context.actorOf(Props[CounterService], name = "counter")
  val totalCount = 51

  import context.dispatcher

  // Use this Actors’ Dispatcher as ExecutionContext
  def receive = LoggingReceive {
    case Start if progressListener.isEmpty =>
      progressListener = Some(sender())
      context.system.scheduler.schedule(Duration.Zero, 1 second, self, Do)
    case Do =>
      counterService ! Increment(1)
      counterService ! Increment(1)
      counterService ! Increment(1)
      // Send current progress to the initial sender
      counterService ? GetCurrentCount map {
        case CurrentCount(_, count) => Progress(100.0 * count / totalCount)
      } pipeTo progressListener.get
  }
}

object CounterService {

  final case class Increment(n: Int)

  case object GetCurrentCount

  final case class CurrentCount(key: String, count: Long)

  class ServiceUnavailable(msg: String) extends RuntimeException(msg)

  private case object Reconnect

}

/**
 * Adds the value received in ‘Increment‘ message to a persistent
 * counter. Replies with ‘CurrentCount‘ when it is asked for ‘CurrentCount‘.
 * ‘CounterService‘ supervise ‘Storage‘ and ‘Counter‘.
 */
class CounterService extends Actor {

  import CounterService._
  import Counter._
  import Storage._

  // Restart the storage child when StorageException is thrown.
  // After 3 restarts within 5 seconds it will be stopped.
  override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 3,
    withinTimeRange = 5 seconds) {
    case _: Storage.StorageException => Restart
  }
  val key = self.path.name
  var storage: Option[ActorRef] = None
  var counter: Option[ActorRef] = None
  var backlog = IndexedSeq.empty[(ActorRef, Any)]
  val MaxBacklog = 10000

  import context.dispatcher

  // Use this Actors’ Dispatcher as ExecutionContext
  override def preStart() {
    initStorage()
  }

  /**
   * The child storage is restarted in case of failure, but after 3 restarts,
   * and still failing it will be stopped. Better to back-off than continuously
   * failing. When it has been stopped we will schedule a Reconnect after a delay.
   * Watch the child so we receive Terminated message when it has been terminated.
   */
  def initStorage() {
    storage = Some(context.watch(context.actorOf(Props[Storage], name = "storage")))
    // Tell the counter, if any, to use the new storage
    counter foreach {
      _ ! UseStorage(storage)
    }
    // We need the initial value to be able to operate
    storage.get ! Get(key)
  }

  def receive = LoggingReceive {
    case Entry(k, v) if k == key && counter == None =>
      // Reply from Storage of the initial value, now we can create the Counter
      val c = context.actorOf(Props(classOf[Counter], key, v))
      counter = Some(c)
      // Tell the counter to use current storage
      c ! UseStorage(storage)
      // and send the buffered backlog to the counter
      for ((replyTo, msg) <- backlog) c.tell(msg, sender = replyTo)
      backlog = IndexedSeq.empty
    case msg@Increment(n) => forwardOrPlaceInBacklog(msg)

    case msg@GetCurrentCount => forwardOrPlaceInBacklog(msg)
    case Terminated(actorRef) if Some(actorRef) == storage =>
      // After 3 restarts the storage child is stopped.
      // We receive Terminated because we watch the child, see initStorage.
      storage = None
      // Tell the counter that there is no storage for the moment
      counter foreach {
        _ ! UseStorage(None)
      }
      // Try to re-establish storage after while
      context.system.scheduler.scheduleOnce(10 seconds, self, Reconnect)
    case Reconnect =>
      // Re-establish storage after the scheduled delay
      initStorage()
  }

  def forwardOrPlaceInBacklog(msg: Any) {
    // We need the initial value from storage before we can start delegate to
    // the counter. Before that we place the messages in a backlog, to be sent
    // to the counter when it is initialized.
    counter match {
      case Some(c) => c forward msg
      case None =>
        if (backlog.size >= MaxBacklog)
          throw new ServiceUnavailable(
            "CounterService not available, lack of initial value")
        backlog :+= (sender() -> msg)
    }
  }
}

object Counter {

  final case class UseStorage(storage: Option[ActorRef])

}

/**
 * The in memory count variable that will send current
 * value to the ‘Storage‘, if there is any storage
 * available at the moment.
 */
class Counter(key: String, initialValue: Long) extends Actor {

  import Counter._
  import CounterService._
  import Storage._

  var count = initialValue
  var storage: Option[ActorRef] = None

  def receive = LoggingReceive {
    case UseStorage(s) =>
      storage = s
      storeCount()
    case Increment(n) =>
      count += n
      storeCount()
    case GetCurrentCount =>
      sender() ! CurrentCount(key, count)
  }

  def storeCount() {
    // Delegate dangerous work, to protect our valuable state.
    // We can continue without storage.
    storage foreach {
      _ ! Store(Entry(key, count))
    }
  }
}

object DummyDB {

  import Storage.StorageException

  private var db = Map[String, Long]()

  @throws(classOf[StorageException])
  def save(key: String, value: Long): Unit = synchronized {
    if (11 <= value && value <= 14)
      throw new StorageException("Simulated store failure " + value)
    db += (key -> value)
  }

  @throws(classOf[StorageException])
  def load(key: String): Option[Long] = synchronized {
    db.get(key)
  }
}

object Storage {

  final case class Store(entry: Entry)

  final case class Get(key: String)

  final case class Entry(key: String, value: Long)

  class StorageException(msg: String) extends RuntimeException(msg)

}

/**
 * Saves key/value pairs to persistent storage when receiving ‘Store‘ message.
 * Replies with current value when receiving ‘Get‘ message.
 * Will throw StorageException if the underlying data store is out of order.
 */
class Storage extends Actor {

  import Storage._

  val db = DummyDB

  def receive = LoggingReceive {
    case Store(Entry(key, count)) => db.save(key, count)
    case Get(key) => sender() ! Entry(key, db.load(key).getOrElse(0L))
  }
}

这个例子定义了五个Actor,分别是Worker, Listener, CounterService ,Counter 和 Storage,下图给出了系统正常运行时的流程(无错误发生的情况):
20140830001

 

其中Worker是CounterService的父Actor(管理员),CounterService是Counter和Storage的父Actor(管理员)图中浅红色,白色代表引用,其中Worker引用了Listener,Listener也引用了Worker,它们之间不存在父子关系,同样Counter也引用了Storage,但Counter不是Storage的管理员。

正常流程如下:

步骤 描述
1 progress Listener 通知Worker开始工作.
2 Worker通过定时发送Do消息给自己来完成工作
3,4,5 Worker接受到Do消息时,通知其子Actor CounterService 三次递增计数器,

CounterService 将Increment消息转发给Counter,它将递增计数器变量然后把当前值发送给Storeage保存

6,7  Workier询问CounterService 当前计数器的值,然后通过管道把结果传给Listener

下图给出系统出错的情况,例子中Worker和CounterService作为管理员分别定义了两个管理策略,Worker在收到CounterService 的ServiceUnaviable上终止CounterService的运行,而CounterService在收到StorageException时重启Storage。

20140830002

 

出错时的流程

步骤 描述
1  Storage抛出StorageException异常
2  Storage的管理员CounterService根据策略在接受到StorageException异常后重启Storage
3,4,5,6  Storage继续出错并重启
7  如果在5秒钟之内Storage出错三次并重启,其管理员(CounterService)就终止Storage运行
8  CounterService 同时监听Storage的Terminated消息,它在Storeage终止后接受到Terminated消息
9,10,11  并且通知Counter 暂时没有Storage
12  CounterService 延时一段时间给自己发生Reconnect消息
13,14  当它收到Reconnect消息时,重新创建一个Storage
15,16  然后通知Counter使用新的Storage

这里给出运行的一个日志供参考。

 

一个使用sbt编译的JNI C++ 的模板

如果你需要在Scala或是Java中调用C或C++函数库,就需要使用JNI, 这里就涉及到编译scala ,java 和C(C++)代码,在这里给出一个程序的框架,我们使用sbt 缺省的代码目录

文件目录
src
—>main
——–>java
——–>scala
——–>c

其中目录c存放C++代码 ,java目录放置Java代码, scala目录放置Scala代码

项目组用来编译的相关文件为build.sbt 和Makefile (它编译放置在c目录下的C++文件,注意只能编译C++,如果你有需要编译C,需要自行修改Makefile)

修改库文件名为自己所需的名称
修改build.sbt 中的项目名称

name := "JNIDemo"

version := "0.0.1-SNAPSHOT"

organization := "com.guidebee"

修改Makefile的库文件名称

#### PROJECT SETTINGS ####
# The name of the executable to be created
SODIR = target/so
BIN_NAME= $(SODIR)/libjnidemo.so

修改Java引用的库文件名称:

static {
    System.loadLibrary("jnidemo");
  }

设置环境变量
在你设置好sbt的编译环境后,注意设置 JAVA_HOME 和 LD_LIBRARY_PATH 环境变量 ,Java_HOME为你Java的安装目录, 可以使用
export LD_LIBRARY_PATH = $LD_LIBRARY_PATH:./:./target/so
将编译后的库文件添加到 java.library.path 路径中(这样Java代码可以找到库文件所在目录)

编译和运行
编译使用指令 sbt compile

root@ubuntu:/sdb/jni# sbt compile
[info] Set current project to JNIDemo (in build file:/mnt/sdb1/jni/)
[info] Compiling 3 Java sources to /mnt/sdb1/jni/target/scala-2.10/classes...
Creating directories
Beginning release build
Compiling: src/main/c/IntArray.cpp -> build/release/IntArray.o -Wall -Wextra -g -fPIC -c -O -m64 -Wunused-parameter
src/main/c/IntArray.cpp:21:1: warning: unused parameter ‘obj’ [-Wunused-parameter]
 Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
 ^
         Compile time: 00:00:00
Compiling: src/main/c/Prompt.cpp -> build/release/Prompt.o -Wall -Wextra -g -fPIC -c -O -m64 -Wunused-parameter
src/main/c/Prompt.cpp: In function ‘_jstring* Java_Prompt_getLine(JNIEnv*, jobject, jstring)’:
src/main/c/Prompt.cpp:24:13: warning: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]
   char *buf="hello";
             ^
src/main/c/Prompt.cpp: At global scope:
src/main/c/Prompt.cpp:22:1: warning: unused parameter ‘obj’ [-Wunused-parameter]
 Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
 ^
         Compile time: 00:00:00
mkdir target/so
Linking: target/so/libjnidemo.so
#@g++ build/release/IntArray.o build/release/Prompt.o  -fPIC    -o target/so/libjnidemo.so
         Link time: 00:00:00
Making library: target/so/libjnidemo.so -> target/so/libjnidemo.so
Total build time: 00:00:00
[success] Total time: 1 s, completed 29/08/2014 10:10:19 PM

运行
sbt run

root@ubuntu:/sdb/jni# sbt run
[info] Set current project to JNIDemo (in build file:/mnt/sdb1/jni/)
[info] Running Test 
[info] User typed: hello
[info] sum = 45
[info] Type a line: 
[success] Total time: 0 s, completed 29/08/2014 10:10:54 PM

删除编译结果
sbt clean

root@ubuntu:/sdb/jni# sbt clean
[info] Set current project to JNIDemo (in build file:/mnt/sdb1/jni/)
[info] Updating {file:/mnt/sdb1/jni/}jni...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
Deleting target/so/libjnidemo.so symlink
Deleting directories
[success] Total time: 1 s, completed 29/08/2014 10:11:35 PM

Github 代码
本例模板代码可以在 https://github.com/guidebee/JNIDemo下载。

20140829001

 

Play Framework Web开发教程(23): 将HTTP请求路由到Controller的Action方法

一旦你定义好Controller类和其中的Action方法,你需要有一个方法来把不同HTTP请求(URL)映射到不同的Action方法,在Play应用中,这种映射称为路由Routing(和网络路由使用同一个术语,但意思稍有不同),Play的路由器负责把HTTP请求映射到对于的Action方法,并调用该Action方法,它同时也把HTTP请求的参数绑定到action方法的参数。
下图给出了包含路由的从GET/Products 到控制器方法 Products.list
201400828001
配置路由
Play路由配置无需使用代码编程实现,路由的配置可以通过路由配置文件(conf/routes)来实现,这个文件为一文本文件,定义了路由,这样的实现有一个好处是,你应用定义的所有公开的HTTP接口都定义在同一个文件中,便与维护。
因此我们可以设计如下的URL规则:

201400828002

根据这些URL设计,我们可以定义路由,定义路由的一般语法如下:
201400828003

定义路由规则是一个规则一行,规则之间可以有空行和注释。

比如,我们把/ 映射到 Products控制类的home方法
GET / controllers.Products.home()
类似的产品列表
GET /products controllers.Products.list()
如果一个Action方法定义了参数,路由器可以把请求字符串中的查询参数绑定到Action方法的参数,比如我们为GET /products 加一个可选的页码参数:
GET /products controllers.Products.list(page: Int ?= 1)
其中=? 不是Scala的语法,只用在路由配置文件中,代表参数可选,
URL规则也可以定义参数,比如查询产品时,URL中需要提供产品的ean代码:
GET /product/:ean controllers.Products.details(ean: Long)

综上,我们定义的路由配置如下:

GET / controllers.Application.home()
GET /products controllers.Products.list(page: Int ?= 1)
GET /product/:ean controllers.Products.details(ean: Long)
GET /product/:ean/edit controllers.Products.edit(ean: Long)
PUT /product/:ean controllers.Products.update(ean: Long)

如何匹配参数包含“/”的情况
URL路径通常使用”/” 来分隔参数,比如之前的/product/5010255079763/edit ,其中的13为数字为一参数,但如果碰到参数值包含“/”的情况会出现什么样的情况?
比如我们需要扩展HTTP接口来支持产品的照片,图片可能的URL如下:

/photo/5010255079763.jpg
/photo/customer-submissions/5010255079763/42.jpg
/photo/customer-submissions/5010255079763/43.jpg

你可能会试着使用如下的路由配置来匹配图片的路径:

GET /photo/:file controllers.Media.photo(file: String)

这个配置,只能匹配第一种情况,后面两个无法匹配,也就是所file:String无法匹配包含“/”的情况。解决方法是使用”*”来替代”:”,*有点带有通配符的意思:

GET /photo/*file controllers.Media.photo(file: String)

这样修改后,file就可以匹配所有/photo之后的内容,包含“/”。

使用正规表达式限定参数
比如你在设计URL规范时,除了使用/product/5010255079763 来查询产品之外,你还想使用好记的名字来查询该产品,比如/product/paper-clips-large-plain-1000-pack
你可能会设计如下的路由规范来匹配:

GET /product/:ean controllers.Products.details(ean: Long)
GET /product/:alias controllers.Products.alias(alias: String)

这个路由配置确不工作,但你使用/product/paper-clips-large-plain-1000-pack 它会使用第一条规则,路由器会尝试把paper-clips-large-plain-1000-pack 转成整数,但不成功,系统会报错:

For request GET /product/paper-clips-large-plain-1000-pack
[Can’t parse parameter ean as Long: For input string:
“paper-clips-large-plain-1000-pack”]

无法把字符串转成Long整数,解决方法是为参数添加匹配时的限定,对第一个规则使用正规表达式限定为13位整数:

GET /product/$ean<\d{13}> controllers.Products.details(ean: Long)

在样在匹配/product/paper-clips-large-plain-1000-pack 发现 paper-clips-large-plain-1000-pack 不是个13位整数,规则不适用,接着使用后面一个URL规则匹配,从而可以正确匹配。

 

Play Framework Web开发教程(22): Controllers–HTTP和Scala之间的接口

Play中的控制器(Controller)是用来处理HTTP请求(以URL标识的资源),例如在一个Play应用中我们可以定义如下一些URL:

/products
/product/5010255079763
/product/5010255079763/edit

使用Play应用的路由配置,我们可以把这些URL映射到某个Controller的方法,一个URL对应一个方法。
Controller类和Action方法
根据前面定义的URL,我们可以先开始定义一个Products控制类,它将包含四个Action方法分别来处理不同的请求:list, details, edit和update,如下图:
201400827001
比如其中的list方法,用来处理/products HTTP请求,返回一个产品列表等。
一个控制类在Play应用中,是一个派生于play.api.mvc.Controller类的对象,这个类提供了很多帮助编写Action方法的函数,尽管小的应用可能只需要使用一个Controller,当大多数情况使用多个Controller,每个Controller包含多个相关的Action方法。
一个Action为Controller中定义的一个方法,它返回一个类型为play.api.mvc.Action的对象,你可以使用如下方法来定义一个Action方法:

def list = Action { request =>
	NotImplemented
}

这定义了一个由Request => Result 的函数,用来处理一个HTTP请求并返回HTTP响应。这里NotImplemented为预定义的一个函数,它返回一个501 HTTP响应表明该方法尚未实现。Action方法也可以使用参数,它传入解析后的HTTP请求中的参数。比如,如果你需要实现分页,你使用了一个pageNumber作为参数:

def list(pageNumber: Int) = Action {
	NotImplemented
}

通常Action函数体中根据请求,读取或者跟新数据模型(Data Model),然后显示页面。 下面列出了我们之前需要定义的几个函数的框架:

package controllers
import play.api.mvc.{Action, Controller}
object Products extends Controller {
	def list(pageNumber: Int) = Action {
		NotImplemented
	}
	def details(ean: Long) = Action {
		NotImplemented
	}
	def edit(ean: Long) = Action {
		NotImplemented
	}
	def update(ean: Long) = Action {
		NotImplemented
	}
}

这四个Action方法,对应到下面三个URL

/products
/product/5010255079763
/product/5010255079763/edit

没有第四个URL,这是因为其中update的URL也是有第二个URL,但它使用HTTP PUT方法,而不是使用GET方法,(有兴趣的可以参考REST设计),我们可以使用不同的HTTP方法对同一个URL标识的资源进行不同的操作。通常Web 浏览器可能只支持 GET和POST方法,如果你需要使用PUT 和 DELETE方法 的请求,你需要使用不同的客户端,比如使用Javascript代码。
注:按照惯例,我们通常使用复数名词作为Controller的类名称,比如之前的Products,而定义DataModel时,可能使用其单数形式比如Product。
Play应用中,通常使用Object来定义一个Controller对象,这是因为Controller不保存任何状态,它的作用是将多个Action分组,这也是你能看到的无状态的MVC模型的一个方面。因此在一个Controller对象中不要使用var来定义成员,它的成员只可以使用val来定义。

HTTP以及Controller层面上的部分Scala API
Play应用将controllers, actions, request,和 response作为 trait来定义,它们都定义在play.api.mvc 包中。
注意:只引入play.api 包 ,play.mvc中定义的类和Scala Play应用无关(Java Play相关类)
下面一些trait和类封装了HTTP相关的一个概念,比如请求,响应和cookie

  • play.api.mvc.Cookie—HTTP cookie: 保存在客户端少量数据,可以作为请求的一部分。
  • play.api.mvc.Request—HTTP 请求: HTTP 方法, URL, headers, body,以及 cookies
  • play.api.mvc.RequestHeader—Request 元数据: 一个 name-value 对
  • play.api.mvc.Response—一个HTTP 响应, with headers and a body;
  • play.api.mvc.ResponseHeader—Response 元数据: a name-value 对

Controller API也定义自己的一些概念,比如Call,其它一些代表了额外的控制器功能,比如Flash,下面列出一些相关的类或trait:

  • play.api.mvc.Action— 一个函数,处理来自客户端的请求然后返回结果
  • play.api.mvc.Call— 一个HTTP 请求: 包含一个HTTP方法和URL
  • play.api.mvc.Content—一个HTTP 响应 ,包含content type和消息体
  • play.api.mvc.Controller—Controller 对象的基类
  • play.api.mvc.Flash— 一个临时数据存储,只对下一个请求有效,其它功能类似于Session。
  • play.api.mvc.Result— Action处理请求之后的返回值类型
  • play.api.mvc.Session—一组键-值, 保存在Cookie中

组合使用Action
有些时候,在多个Action方法中可能需要一些公用的函数,这可能会导致重复代码,比如访问控制,或者缓存一个返回结果以提高响应速度。一个常用的方法,是就这些公用的代码抽取出来定义成函数,比如:

def list = Action {
// Check authentication.
// Check for a cached result.
// Process request...
// Update cache.
}

但使用Scala我们有更好的方法,这意味着我们可以将多个Action方法组合起来使用,比如:

def list =
	Authenticated {
		Cached {
			Action {
				// Process request...
			}
	}
}

这个例子,我们使用Action创建了一个函数,然后又传给Cached,紧接着又传给Authenticated.

 

Play Framework Web开发教程(21): 设计Web应用的URL方案

我们前面概要介绍了Play应用的各个部分,Play也是基于MVC模型,从本篇其我们将详细介绍MVC的View和Controller,至于Model本教程不再详细介绍,可以参考Slick开发教程, 使用的例子还是使用我们前面提到的产品目录的例子。
具体实现特定的URL
如果你之前使用Structs 1.x开发过Web应用,那么你看到过Web框架特定的URL定义,Structs曾经非常流行,但目前来说已经过时了。Struct1.x 定义了一个基于Action的MVC结构(本身和Play并无太大差异)。 这意味着如果需要显示产品详细信息,我们可以在Java类中定义ProductDetailsAction 方法,然后可以使用如下的URL来访问:
/product.do
在这个URL中,.do扩展名表示Web框架应该把这个请求映射到一个Action类,而product表面了这个Action类型的名称。
我们可能需要指明具体哪个产品,比如可以指明产品唯一的EAN代码:
/product.do?ean=5010255079763
之后,我们可以需要扩展Action类以支持其它方法,比如支持编辑产品信息,我们可以使用如下的URL
/product.do?ean=5010255079763&method=edit
当我们使用这种方法构造Web应用时,它工作得很好,至少在某种程度上来说。
但是这种URL是和Web应用的具体实现相关的(比如使用的Web框架是struct 1.x),首先 .do 没有任何意义,它存在的意义只是为了使用HTTP到Java接口能够工作,不同Web框架可能使用不同的定义。 其次method=edit 请求参数是用来使用Struct的某个功能,如果修改了Web应用,你可能需要修改URL为:
/productEdit.do?ean=5010255079763
也许你不介意修改URL,但是URL变化不是件好事情,比如搜寻引擎保存的之前的URL在修改之后就不能工作,或者用户保存的书签也不能工作了等等,因此我们需要设计一个不经常变化的URL方案。
稳定的URL
一旦你理解了使用稳定URL的需要,我们需要设计它们,它也是API设计的一部分,和面向对象设计时定义公共接口没有太大差别。
设计稳定URL要避免使用任何实现紧密相关的内容,对于之前的设计,我们可以设计如下的URL方案:

/products –产品列表
/product/5010255079763 –单个产品详细信息
/product/5010255079763/edit –修改单个产品

(这其实是采用REST API的设计)

良好URL方案的优势

  • 下面给出一个设计优良的URL方案可能带来的好处
  • 始终如一的公共API接口 — 这种URL方案可以使你的Web应用易于理解。
  • 这种URL不经常变换 — 避免使用具体实现(Web框架相关)的URL,可以使得URL变得更加稳定,因此即使实现Web应用的技术发生了变化,URL也可能保存不变。
  • 短URL –短小的URL更加有效,它们易于输入或者复制到其它媒体(比如Email或及时消息中)