You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Spray反序列化带类型参数Trait时如何实现外部调用分页

解决方案

核心问题出在你尝试将带泛型参数的特质作为Spray JSON的反序列化目标,Spray需要具体case class对应的隐式JsonReader实例,无法识别泛型特质。我们可以通过给泛型加上界的方式解决,完全保留原有代码结构,修改量极小:

步骤1:优化分页特质定义

// 泛型T为实现该特质的具体列表包装类本身,保证append方法返回类型和实现类一致
trait Pagination[T <: Pagination[T]] {
  // 获取当前页数据条数
  def getSize(): Int
  // 合并当前实例与另一页数据,返回新的合并后实例
  def append(other: Option[T]): Option[T]
}

步骤2:修改现有列表包装类实现分页特质

原有样例类不需要修改原有字段结构,仅增加特质继承和方法实现即可:

case class Users(users: Array[User]) extends Pagination[Users] {
  override def getSize(): Int = users.length
  override def append(other: Option[Users]): Option[Users] = other match {
    case Some(otherPage) => Some(Users(this.users ++ otherPage.users))
    case None => Some(this)
  }
}

case class Countries(countries: Array[Country]) extends Pagination[Countries] {
  override def getSize(): Int = countries.length
  override def append(other: Option[Countries]): Option[Countries] = other match {
    case Some(otherPage) => Some(Countries(this.countries ++ otherPage.countries))
    case None => Some(this)
  }
}

// 修正你原有代码的笔误:states字段类型应为Array[State]而非Array[States]
case class States(states: Array[State]) extends Pagination[States] {
  override def getSize(): Int = states.length
  override def append(other: Option[States]): Option[States] = other match {
    case Some(otherPage) => Some(States(this.states ++ otherPage.states))
    case None => Some(this)
  }
}

步骤3:修改通用拉取类的泛型约束

仅需要给GenericHttpDataPuller的列表泛型V增加上界约束,要求V必须实现Pagination特质,同时保留原有JsonReader上下文绑定,保证反序列化时使用具体case class的序列化器:

import spray.json.JsonReader
import akka.http.scaladsl.model._
import scala.util.Try
import scala.util.Success
import scala.util.Failure

// 新增V的上界约束,其他原有定义保持不变
class GenericHttpDataPuller[U: JsonReader, V <: Pagination[V]: JsonReader](val endPoint: String) {
  // 原有getEntity方法完全不变
  def getEntity[T: JsonReader](request: HttpRequest): Option[T] = {
    Try(httpClient.singleRequest(request).entity.parseJson.convertTo[T]) match {
      case Success(entity) => Some(entity)
      case Failure(_) => None
    }
  }

  // 原有get方法完全不变,兼容原有调用逻辑
  def get(id: String, options: Map[String, String] = Map.empty): Option[U] = {
    val uri = Uri(s"/$endPoint/$id").withQuery(Uri.Query(options))
    val httpRequest = HttpRequest(HttpMethods.GET, uri = uri, headers = additionalHeaders)
    getEntity[U](httpRequest)
  }

  // 改造getList方法实现分页,返回类型仍为Option[V],完全兼容原有调用
  def getList(options: Map[String, String] = Map.empty): Option[V] = {
    val limit = Try(options.getOrElse("limit", "20").toInt).getOrElse(20)
    var currentPage = 1

    // 拉取指定页码的单页数据
    def fetchPage(pageNum: Int): Option[V] = {
      val pageParams = options + ("page" -> pageNum.toString, "limit" -> limit.toString)
      val uri = Uri(s"/$endPoint").withQuery(Uri.Query(pageParams))
      val request = HttpRequest(HttpMethods.GET, uri = uri, headers = additionalHeaders)
      getEntity[V](request)
    }

    // 递归合并分页数据
    def mergePages(acc: Option[V]): Option[V] = {
      val pageData = fetchPage(currentPage)
      pageData match {
        // 当前页数据条数等于分页大小,继续拉取下一页
        case Some(page) if page.getSize() == limit =>
          currentPage += 1
          mergePages(acc.flatMap(_.append(pageData)))
        // 当前页数据不足分页大小,结束分页
        case Some(page) =>
          acc.flatMap(_.append(pageData))
        // 拉取失败直接返回已有数据
        case None =>
          acc
      }
    }

    mergePages(None)
  }
}

步骤4:调用端完全无需修改

原有调用逻辑可以直接复用,不需要任何改动:

type HttpUserDataPuller = GenericHttpDataPuller[User, Users]
// 分页拉取全量用户数据
val allUsers = new HttpUserDataPuller("users").getList(Map("limit" -> "50"))
// 原有单用户查询逻辑不变
val singleUser = new HttpUserDataPuller("users").get("someId")

方案优势

  • 改动极小:仅给原有列表样例类增加了特质实现,修改了拉取类的泛型约束,原有业务逻辑完全不需要调整
  • 无序列化问题:反序列化直接使用具体的列表包装类(如Users、Countries)作为目标类型,Spray可以正常匹配到对应的隐式JsonReader,不会报错
  • 接口完全兼容:对外暴露的get、getList方法签名和原有完全一致,不会影响现有业务代码的调用

内容的提问来源于stack exchange,提问作者Puneeth Reddy V

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 05:36:03