4

我正在使用 Play 的 WSClient 与第三方服务进行交互

request = ws.url(baseUrl)
  .post(data)
  .map{ response =>
     response.json.validate[MyResponseClass]

响应可能是MyResponseClass或可能是ErrorResponse类似{ "error": [ { "message": "Error message" } ] }

是否有解析类错误的典型方法?

我应该做这样的事情吗?

response.json.validateOpt[MyResponseClass].getOrElse(response.json.validateOpt[ErrorClass])
4

2 回答 2

5

这个问题没有单一的答案。这里有多个微妙的考虑。我的回答将尝试提供一些方向。

至少要处理四种不同的情况

  1. 应用程序级有效结果(建立连接、收到响应、200 状态码)
  2. 应用程序级错误(已建立连接、收到响应、4xx、5xx 状态代码)
  3. 网络 IO 错误(未建立连接,或由于超时等原因未收到响应)
  4. JSON 解析错误(已建立连接、收到响应、无法将 JSON 转换为模型域对象)

伪代码

  1. 完成Future后的响应是ErrorResponseor MyResponseClass,即Either[ErrorResponse, MyResponseClass]:

    1. 如果服务返回 200 状态码,则解析为MyResponseClass
    2. 如果服务返回 >= 400 状态码,则解析为ErrorResponse
  2. 完成Future但内部异常:

    1. 解析异常,或
    2. 网络 IO 异常(例如超时)

Future(Left(errorResponse))对比Future(throw new Exception)

Future(Left(errorResponse))注意和之间的区别Future(throw new Exception):我们只将后者视为失败的未来。前者,尽管有Left内幕,仍然被认为是一个成功完成的未来。

Future.andThen对比Future.recover

Future.andThen注意and之间的区别Future.recover:前者不会改变未来内部的值,而后者可以改变内部的值及其类型。如果无法恢复,我们至少可以使用andThen.

示例

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import play.api.libs.ws._
import play.api.libs.ws.ahc._
import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.Future
import play.api.libs.json._
import play.api.libs.ws.JsonBodyReadables._
import scala.util.Failure
import java.io.IOException
import com.fasterxml.jackson.core.JsonParseException

case class ErrorMessage(message: String)

object ErrorMessage {
  implicit val errorMessageFormat = Json.format[ErrorMessage]
}

case class ErrorResponse(error: List[ErrorMessage])

object ErrorResponse {
  implicit val errorResponseFormat = Json.format[ErrorResponse]
}

case class MyResponseClass(a: String, b: String)

object MyResponseClass {
  implicit val myResponseClassFormat = Json.format[MyResponseClass]
}

object PlayWsErrorHandling extends App {
    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()

    val wsClient = StandaloneAhcWSClient()

    httpRequest(wsClient) map {
      case Left(errorResponse) =>
        println(s"handle application level error: $errorResponse")
        // ...

      case Right(goodResponse) =>
        println(s"handle application level good response $goodResponse")
        // ...

    } recover { // handle failed futures (futures with exceptions inside)
      case parsingError: JsonParseException =>
        println(s"Attempt recovery from parsingError")
        // ...

      case networkingError: IOException =>
        println(s"Attempt recovery from networkingError")
        // ...
    }

  def httpRequest(wsClient: StandaloneWSClient): Future[Either[ErrorResponse, MyResponseClass]] =
    wsClient.url("http://www.example.com").get() map { response ⇒

      if (response.status >= 400) // application level error
        Left(response.body[JsValue].as[ErrorResponse])
      else // application level good response
        Right(response.body[JsValue].as[MyResponseClass])

    } andThen { // exceptions thrown inside Future
      case Failure(exception) => exception match {
        case parsingError: JsonParseException => println(s"Log parsing error: $parsingError")
        case networkingError: IOException => println(s"Log networking errors: $networkingError")
      }
    }
}

依赖项:

libraryDependencies ++= Seq(
  "com.typesafe.play" %% "play-ahc-ws-standalone"   % "1.1.3",
  "com.typesafe.play" %% "play-ws-standalone-json"  % "1.1.3"
)
于 2017-11-10T01:40:44.250 回答
2

有一个Either[L, R]非常适合您可以拥有一个值或另一个值(不是两者,也不是都没有)的情况,您可以做这样的事情(未经测试):

val result: Option[Either[ErrorClass, MyResponseClass]] = response
  .json
  .validateOpt[MyResponseClass]
  .map { resp => Right(resp) }
  .orElse {
    response.json.validateOpt[ErrorClass]
      .map { error => Left(error) }
  }

将错误结果存储在左侧,将成功存储在右侧是一种常见的模式。

于 2017-11-09T21:36:26.143 回答