使用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
相关产品推荐
相关产品推荐

