如何在Play框架Action中保存状态并重构重复代码块?
Great question! Extracting repetitive boilerplate like shared setup code and exception handling into a custom Play Action is a perfect way to keep your controllers clean, focused on business logic, and maintainable. Let's walk through how to do this step by step.
第一步:创建自定义ActionBuilder
We'll build a custom ActionBuilder that wraps your commonCode execution and centralized exception handling. This way, every action using this builder automatically gets that behavior without repeating code.
基础版本(无状态传递)
If your commonCode doesn't need to pass state to the specific action logic, this simple version works:
import play.api.mvc._ import scala.concurrent.{ExecutionContext, Future} // Make this injectable so we can use it in controllers class MyCustomAction @Inject()(val parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilder[Request, AnyContent] { override def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] = { try { // Run your shared commonCode first commonCode(request) // Execute the action-specific logic block(request) } catch { // Centralized exception handling case mx: MyException => Future.successful(mx.toResult()) case t: Throwable => Future.successful(BadRequest(s"Unexpected error: ${t.getMessage}")) } } // Move your shared commonCode here private def commonCode[A](request: Request[A]): Unit = { // Your original setup logic (create state, initialize resources, etc.) // Example: val sessionState = SessionState.fromRequest(request) } }
进阶版本(传递状态给业务逻辑)
If your commonCode creates a state that the action-specific logic needs to use, we'll extend ActionTransformer to wrap the request with your state:
First, define a wrapped request type to hold your state:
// Custom request type that includes your state case class RequestWithState[A](state: MyState, request: Request[A]) extends WrappedRequest[A](request) // Your state class (adjust based on your actual needs) case class MyState(userId: Option[String], sessionData: Map[String, String])
Then update the custom action to create and pass the state:
class MyCustomAction @Inject()(val parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilder[RequestWithState, AnyContent] with ActionTransformer[Request, RequestWithState] { override def transform[A](request: Request[A]): Future[RequestWithState[A]] = { try { // Create your state using commonCode logic val state = createStateFromRequest(request) Future.successful(RequestWithState(state, request)) } catch { case mx: MyException => Future.failed(mx) case t: Throwable => Future.failed(new RuntimeException("Failed to initialize request state", t)) } } override def invokeBlock[A](request: Request[A], block: RequestWithState[A] => Future[Result]): Future[Result] = { // Transform the request, run the business logic, and handle exceptions centrally transform(request).flatMap(block).recover { case mx: MyException => mx.toResult() case t: Throwable => BadRequest(s"Unexpected error: ${t.getMessage}") } } private def createStateFromRequest[A](request: Request[A]): MyState = { // Your original commonCode logic to build the state MyState( userId = request.session.get("userId"), sessionData = request.session.data ) } }
第二步:在控制器中使用自定义Action
Now your controller becomes much cleaner—you only need to write the action-specific logic, and the custom action handles the rest:
class MyController @Inject()(myCustomAction: MyCustomAction, cc: ControllerComponent) extends AbstractController(cc) { // Use your custom action instead of the default Play Action def get(name: String) = myCustomAction { request => // If using the stateful version, access the state via request.state val response = actionSpecificCode(request) Ok(response) } // Your original action-specific logic, now focused solely on business needs private def actionSpecificCode[A](request: Request[A]): String = { // Example business logic s"Hello, $name! Your session ID is ${request.session.id}" } }
为什么这很有用?
- 减少重复代码: No more copying try/catch blocks and common setup across every action.
- 统一异常处理: All exceptions follow the same handling logic, making it easier to update later.
- 关注点分离: Controllers focus on business logic, while the custom action handles cross-cutting concerns (setup, error handling).
内容的提问来源于stack exchange,提问作者Igor Urisman

