1. 概述
在本教程中,我们将介绍 Play Framework 默认的错误处理机制,并实现一个自定义的错误处理器。
首先,我们会编写一个返回多种错误的控制器,观察框架是如何自动处理这些错误的;然后,我们将实现一个自定义错误处理器,并分析其局限性。
2. Play Framework 错误类型
在 Play 中,控制器会在以下三种情况下返回错误:
✅ 客户端请求无效,未能通过验证(客户端错误)
✅ 应用代码抛出异常(服务端错误)
✅ 在 Scala 代码中显式返回错误响应(业务相关错误)
3. 控制器错误示例
我们先来准备一个控制器类,用来返回上述三种类型的错误,以便观察默认行为并测试自定义错误处理器。
下面是包含五个方法的控制器类,分别返回不同类型的错误:
class ErrorDemoController @Inject()(
val controllerComponents: ControllerComponents
) extends BaseController {
def notFound(): Action[AnyContent] = Action {
NotFound
}
def exception(): Action[AnyContent] = Action {
throw new RuntimeException("模拟应用内部错误")
Ok // 只是为了匹配返回类型
}
def internalError(): Action[AnyContent] = Action {
InternalServerError
}
def badRequest(): Action[AnyContent] = Action {
BadRequest
}
}
接着,在 routes
文件中添加对应的路由映射:
GET /errors/notfound controllers.ErrorDemoController.notfound()
GET /errors/exception controllers.ErrorDemoController.exception()
GET /errors/internalerror controllers.ErrorDemoController.internalError()
GET /errors/badRequest controllers.ErrorDemoController.badRequest()
4. 默认错误处理机制
现在我们逐个访问这些接口,看看 Play 是如何处理这些错误的。
4.1. 处理 Scala 异常
访问 http://localhost:9000/errors/exception,可以看到我们在代码中**主动抛出了一个 RuntimeException
来模拟应用内部错误**。
在开发模式下,Play 会返回一个展示源码的错误页面,并高亮出错的那行代码:
⚠️ 当然,将源码暴露给用户是严重的安全风险,在生产环境中,错误页面信息会大幅减少:
如错误提示所示,我们可以在日志中看到具体的错误 ID 和堆栈信息:
! @7ibgcn351 - Internal server error, for (GET) [/errors/exception] ->
play.api.UnexpectedException: Unexpected exception[RuntimeException: Pretend that we have an application error.]
at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:355)
...
Caused by: java.lang.RuntimeException: Pretend that we have an application error.
at controllers.ErrorDemoController.$anonfun$exception$1(ErrorDemoController.scala:13)
4.2. 显式返回 InternalServerError、NotFound 和 BadRequest
访问 http://localhost:9000/errors/internalerror,此时我们**在代码中显式返回了 InternalServerError
状态码**,Play 不会展示默认的错误页面,浏览器只会接收到 500 状态码,并显示其默认错误页。
同理,访问 /errors/notfound
和 /errors/badrequest
时,也会收到对应的 404 和 400 状态码,而不会触发错误处理器。
4.3. 真正的 NotFound 页面
当我们访问一个不存在的页面,比如 http://localhost:9000/this_is_not_here,Play 会显示默认的 404 页面(开发模式下):
如果我们希望在页面不存在时跳转到其他页面,就需要自定义错误处理器。
5. 自定义错误处理器
要实现自定义错误处理,我们需要创建一个新的错误处理器类:
class CustomErrorHandler extends HttpErrorHandler {
def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = {
if (statusCode == NOT_FOUND) {
// 仅当访问不存在页面时生效,而不是显式返回 NotFound
Future.successful(Redirect(routes.ErrorDemoController.noError()))
} else {
Future.successful(
Status(statusCode)("客户端错误")
)
}
}
def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = {
Future.successful(
InternalServerError("服务端错误:" + exception.getMessage)
)
}
}
我们希望在页面不存在时跳转到 /errors/noerror
页面,因此需要在控制器中添加新方法:
def noError(): Action[AnyContent] = Action {
Ok
}
并在 routes
文件中新增路由:
GET /errors/noerror controllers.ErrorDemoController.noerror()
最后,在 application.conf
中配置使用自定义处理器:
play.http.errorHandler = "errors.CustomErrorHandler"
6. 测试自定义错误处理器
访问 http://localhost:9000/errors/exception,页面将不再是红色错误页,而是显示:“服务端错误:模拟应用内部错误”。这说明 onServerError
方法被成功调用。
⚠️ 注意,自定义错误处理器同样不会拦截我们显式返回的错误状态码。访问 /errors/internalerror
时,浏览器依然会收到 500 状态码并显示默认错误页。
测试客户端错误时,访问 /errors/notfound
会返回 404 状态码,浏览器显示“页面未找到”。但访问 /this_is_not_here
时,自定义的 onClientError
会被调用,并跳转到 /errors/noerror
页面。
6.1. 编写自动化测试
在编写控制器单元测试时,框架不会使用自定义错误处理器,这是为了确保我们测试的是控制器逻辑,而不是错误处理器。
如果要测试错误处理器本身,需要单独编写单元测试,直接调用 onClientError
或 onServerError
方法:
class CustomErrorHandlerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting with Eventually {
"CustomErrorHandler" should {
"redirect when a page has not been found" in {
//given
val objectUnderTest = new CustomErrorHandler()
val request = FakeRequest(GET, "/fake")
val statusCode = StatusCodes.NotFound
val message = ""
//when
val responseFuture = objectUnderTest.onClientError(request, statusCode.intValue, message)
//then
eventually {
status(responseFuture) mustBe StatusCodes.SeeOther.intValue
}
}
}
}
7. 总结
本文中我们测试了 Play Framework 的默认错误处理机制,通过自定义错误处理器修改了其行为,并验证了显式返回错误状态码不会触发错误处理器这一特性。
源码地址:GitHub 项目