一、自定义action
从一个日志装饰器的例子开始
1.在invokeBlock方法中实现
import play.api.mvc._ class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) { override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = { Logger.info("Calling action") block(request) } }
在控制器中使用依赖注入来得到一个实例
class MyController @Inject()(loggingAction: LoggingAction,cc:ControllerComponents)extends AbstractController(cc) { def index = loggingAction { Ok("Hello World") } }
ActionBuilder提供了所有构建action的方法,因此也适用于自定义的解析器
def submit = loggingAction(parse.text) { request => Ok("Got a body " + request.body.length + " bytes long") }
2.编写Action
可重用的动作代码可以通过包装动作来实现
import play.api.mvc._ case class Logging[A](action: Action[A]) extends Action[A] { def apply(request: Request[A]): Future[Result] = { Logger.info("Calling action") action(request) } override def parser = action.parser override def executionContext = action.executionContext }
我们也可以使用Action
动作构建器来构建动作,而无需定义我们自己的动作类
import play.api.mvc._ def logging[A](action: Action[A])= Action.async(action.parser) { request => Logger.info("Calling action") action(request) }
也可以使用composeAction
方法将操作混合到操作构建器中
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) { override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = { block(request) } override def composeAction[A](action: Action[A]) = new Logging(action) }
现在,建造者可以像以前一样使用
def index = loggingAction {Ok("Hello World")}
我们也可以在没有动作构建器的情况下混合包装动作
def index = Logging { Action { Ok("Hello World") } }
3.更复杂的actions
上面都是不影响请求的操作。当然,我们也可以读取和修改传入的请求对象,play支持X-Forwarded-For
headers
import play.api.mvc._ import play.api.mvc.request.RemoteConnection def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request => val newRequest = request.headers.get("X-Forwarded-For") match { case None => request case Some(xff) => val xffConnection = RemoteConnection(xff, request.connection.secure, None) request.withConnection(xffConnection) } action(newRequest) }
阻断请求
import play.api.mvc._ import play.api.mvc.Results._ def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request => request.headers.get("X-Forwarded-Proto").collect { case "https" => action(request) } getOrElse { Future.successful(Forbidden("Only HTTPS requests allowed")) } }
修改返回的结果
import play.api.mvc._ def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request => action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1")) }
二、不同的请求类型
1.一些预先定义好的实现了ActionFunction的特质
ActionTransformer
可以更改请求,例如增加额外的信息ActionFilter
可以选择性的拦截请求,例如产生错误,不改变请求值ActionRefiner
是上面两种情况的一般情况ActionBuilder
i请求作为输入的特例,因此可以构建actions
2.认证
我们可以轻松实现我们自己的身份验证操作转换器,该转换器根据原始请求确定用户并将其添加到新的UserRequest
。请注意,这也是一个ActionBuilder
因为它使用一个简单Request
作为输入
import play.api.mvc._ class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request) class UserAction @Inject()(val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext) extends ActionBuilder[UserRequest, AnyContent] with ActionTransformer[Request, UserRequest] { def transform[A](request: Request[A]) = Future.successful { new UserRequest(request.session.get("username"), request) } }
Play还提供内置身份验证操作构建器。有关这方面的信息以及如何使用它可以在这里找到。内置的身份验证操作构建器只是一个简便的帮助工具,可以将简单情况下实现身份验证所需的代码最小化,其实现方式与上述示例非常相似。
如果内置的助手不合适,可以自己编写
3.向请求添加信息
现在让我们考虑一个适用于Item对象的REST API 。/item/:itemId下可能有许多路径,每个路径都要查找item。在这种情况下,把下面的逻辑放入一个action函数可能会很有用
首先,我们将创建一个请求对象,把Item添加
到我们的UserRequest
import play.api.mvc._ class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) { def username = request.username }
现在我们将创建一个查询item的action,要么返回一个错误(
Left
)要么返回一个新的ItemRequest
(Right
)。请注意,这个action精炼是定义在在获取item的id的方法中的
def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest] { def executionContext = ec def refine[A](input: UserRequest[A]) = Future.successful { ItemDao.findById(itemId) .map(new ItemRequest(_, input)) .toRight(NotFound) } }
4.验证请求
最后,我们可能需要一个action函数来验证请求是否应该继续。例如,也许我们想要检查UserAction中的用户是否有权限访问ItemAction中的item
,如果没有就报错
def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequest] { def executionContext = ec def filter[A](input: ItemRequest[A]) = Future.successful { if (!input.item.accessibleByUser(input.username)) Some(Forbidden) else None } }
5.现在我们可以将这些action函数连接在一起(从一个ActionBuilder开始
),用andThen
创建一个action
def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext) =(userAction andThen ItemAction(itemId) andThen PermissionCheckAction) { request => request.item.addTag(tag) Ok("User " + request.username + " tagged " + request.item.id) }
Play还提供了一个全局过滤器API,这对于全局交叉问题很有用。
三、语言、内容的设置
1.语言
你可以使用play.api.mvc.RequestHeader#acceptLanguages
从Accept-Language header
中检索到请求可接受的语言列表,并根据其质量值对它们进行排序。Play在play.api.mvc.Controller#lang方法中使用它,
提供一个隐式play.api.i18n.Lang值给你的actions
,因此它们会自动使用最好的语言(如果你的应用程序支持的话,否则使用应用程序的默认语言)
2.内容
类似地,该play.api.mvc.RequestHeader#acceptedTypes
方法给出请求的可接受结果的MIME类型列表。它从Accept header
中检索到它们,并根据它们的质量因子对它们进行排序。
实际上,Accept header
并不包含MIME类型,除了媒体范围(一个接收所有文本结果的请求可能会设置text/*
范围,*/*
范围意味着可接收所有结果类型)。控制器提供更高级的render
方法来帮助您处理媒体范围。例如下面的action定义
val list = Action { implicit request => val items = Item.findAll render { case Accepts.Html() => Ok(views.html.list(items)) case Accepts.Json() => Ok(Json.toJson(items)) } }
Accepts.Html()
和Accepts.Json()
是提取器,测试给定的范围是否匹配text/html
和application/json
。为了确定优先权,该render
方法把play.api.http.MediaRange的部分函数带到
play.api.mvc.Result
,并尝试将其应用到在请求中找到的每个范围。
如果你的函数没有可支持的范围,则返回NotAcceptable。
例如,如果客户端使用以下值作为Accept
header发出请求,Accept
:*/*;q=0.5,application/json
意味着它接受任何结果类型但偏好JSON,上面的代码将返回JSON表示。另一种情况application/xml
意味着它只接受XML,则上面的代码将返回NotAcceptable
。
3.请求提取器
请参阅play.api.mvc.AcceptExtractors.Accepts
对象的API文档,以获取render
方法中开箱即用支持的MIME类型列表。你可以使用play.api.mvc.Accepting
case类轻松地为给定的MIME类型创建自己的提取器,
例如下面的代码创建一个提取器来检查媒体范围是否匹配audio/mp3
MIME类型:
val AcceptsMp3 = Accepting("audio/mp3") render { case AcceptsMp3() => ??? } }
四、错误的处理
1.支持自定义错误处理
如果使用BuiltInComponents
构造自己的应用程序,要覆盖该httpErrorHandler
方法以返回你的自定义处理程序的实例。
如果使用运行时依赖注入(例如Guice),则可以在运行时动态加载错误处理程序。最简单的方法是在根包创建一个类ErrorHandler(实现了
,例如:HttpErrorHandler
)
import play.api.http.HttpErrorHandler import play.api.mvc._ import play.api.mvc.Results._ import scala.concurrent._ import javax.inject.Singleton @Singleton class ErrorHandler extends HttpErrorHandler { def onClientError(request: RequestHeader, statusCode: Int, message: String) = { Future.successful( Status(statusCode)("A client error occurred: " + message) ) } def onServerError(request: RequestHeader, exception: Throwable) = { Future.successful( InternalServerError("A server error occurred: " + exception.getMessage) ) } }
如果你不想将错误处理程序置于根包中,或者你想要为不同的环境配置不同的错误处理程序,你可以通过在application.conf中配置play.http.errorHandler来实现,例如:
play.http.errorHandler = "com.example.ErrorHandler"
2.扩展默认的错误处理程序
Play的默认错误处理提供了许多有用的函数。例如,在开发模式下,当发生服务器错误时,Play将尝试在你的应用程序中找到并渲染导致该异常的代码片段,以便可以快速查看并识别问题。你可能希望在生产中提供自定义服务器错误,同时仍在开发中维护该功能。
为了促进这一点,Play提供了一种DefaultHttpErrorHandler
可以覆盖的便捷方法,以便可以将自定义逻辑与Play现有的行为混合在一起。例如,要仅在生产环境中提供自定义服务器错误消息,而且保留开发错误消息,并且还想提供特定的禁止错误页面
import javax.inject._ import play.api.http.DefaultHttpErrorHandler import play.api._ import play.api.mvc._ import play.api.mvc.Results._ import play.api.routing.Router import scala.concurrent._ @Singleton class ErrorHandler @Inject() ( env: Environment, config: Configuration, sourceMapper: OptionalSourceMapper, router: Provider[Router] ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) { override def onProdServerError(request: RequestHeader, exception: UsefulException) = { Future.successful( InternalServerError("A server error occurred: " + exception.getMessage) ) } override def onForbidden(request: RequestHeader, message: String) = { Future.successful( Forbidden("You're not allowed to access this resource.") ) } }
查看完整的API文档DefaultHttpErrorHandler
以查看可用于覆盖的方法以及如何利用它们