基于Shapeless的Scala Case Class DTO转换问题求助
Scala Case Class 转换:处理字段组合与字段名映射的方案
你提到的基于Shapeless的同名字段转换器仅能处理字段名、类型完全匹配的场景,针对字段组合和字段名映射这两类需求,下面提供几种可行的实现方式:
一、基于Shapeless扩展的类型安全方案
Shapeless的LabelledGeneric可以保留字段的标签信息,我们可以通过扩展它来支持自定义字段映射和组合逻辑,同时保留编译时类型检查。
1. 字段名映射场景实现
通过重写字段的符号标签,将DTO的字段映射到目标Case Class的对应字段:
import shapeless._ import shapeless.labelled.{FieldType, field} object DtoToDomainMapper { // 定义字段映射的类型类 trait FieldMapping[A, B] { def apply(a: A): B } // 单个字段的映射逻辑 implicit def mapSingleField[K <: Symbol, V, TargetK <: Symbol](implicit targetWitness: Witness.Aux[TargetK] ): FieldMapping[FieldType[K, V], FieldType[TargetK, V]] = (sourceField: FieldType[K, V]) => field[TargetK](sourceField) // 递归处理HList的字段映射 implicit val hnilMapping: FieldMapping[HNil, HNil] = _ => HNil implicit def hconsMapping[K <: Symbol, V, T <: HList, TT <: HList](implicit headMap: FieldMapping[FieldType[K, V], TT#Head], tailMap: FieldMapping[T, TT#Tail] ): FieldMapping[FieldType[K, V] :: T, TT] = (hlist: FieldType[K, V] :: T) => headMap(hlist.head) :: tailMap(hlist.tail) // 对外暴露的转换方法 def convert[S, T, SR <: HList, TR <: HList](source: S)(implicit genS: LabelledGeneric.Aux[S, SR], genT: LabelledGeneric.Aux[T, TR], mapping: FieldMapping[SR, TR] ): T = genT.from(mapping(genS.to(source))) } // 使用示例 case class UserDTO(name: String, surname: String, age: Int) case class User(firstName: String, surname: String, age: Int) // 为name到firstName的映射提供隐式证据 implicit val nameToFirstName: DtoToDomainMapper.FieldMapping[ FieldType[Symbol @@ "name", String], FieldType[Symbol @@ "firstName", String] ] = field => field[Symbol @@ "firstName"](field) val dto = UserDTO("John", "Doe", 30) val user = DtoToDomainMapper.convert[UserDTO, User](dto) // 输出:User(John, Doe, 30)
2. 字段组合场景实现
在转换流程中插入自定义逻辑,将多个DTO字段组合成目标字段:
import shapeless._ import shapeless.labelled.{FieldType, field} object DtoToDomainMapper { // 定义字段组合的类型类 trait FieldCombiner[S, T] { def combine(source: S): T } // 针对UserDTO到User的组合逻辑 implicit val userCombiner: FieldCombiner[UserDTO, User] = (dto: UserDTO) => User(dto.name + dto.surname, dto.age) // 对外暴露的转换方法 def convert[S, T](source: S)(implicit combiner: FieldCombiner[S, T]): T = combiner.combine(source) } // 使用示例 case class UserDTO(name: String, surname: String, age: Int) case class User(fullName: String, age: Int) val dto = UserDTO("John", "Doe", 30) val user = DtoToDomainMapper.convert[UserDTO, User](dto) // 输出:User(JohnDoe, 30)
如果需要结合自动同名字段转换+部分字段组合,可以扩展Shapeless的Intersection逻辑,在自动映射后补充组合字段:
import shapeless._ import shapeless.labelled.{FieldType, field} class Converter[T] { def apply[S, SR <: HList, TR <: HList, IR <: HList](source: S)(implicit genS: LabelledGeneric.Aux[S, SR], genT: LabelledGeneric.Aux[T, TR], intersection: Intersection.Aux[SR, TR, IR], align: Align[IR, TR], supplement: Supplement[S, TR] ): T = { val autoMapped = align(intersection(genS.to(source), HNil)) val completeHList = supplement(source, autoMapped) genT.from(completeHList) } } // 定义补充字段的类型类 trait Supplement[S, TR <: HList] { def apply(source: S, partial: TR): TR } // 针对UserDTO到User的补充逻辑 implicit val userSupplement: Supplement[UserDTO, FieldType[Symbol @@ "fullName", String] :: FieldType[Symbol @@ "age", Int] :: HNil] = (dto, partial) => field[Symbol @@ "fullName"](dto.name + dto.surname) :: partial.tail // 使用 val converter = new Converter[User] val user = converter(UserDTO("John", "Doe", 30))
二、类似Quill Alias的DSL方案
实现轻量级DSL,让你像Quill那样直观声明字段映射和组合规则:
case class MappingRule[S](rules: List[(S => Any, String)]) object MappingDSL { // 字段名映射规则 def alias[S](sourceField: S => Any)(targetField: String): MappingRule[S] = MappingRule(List((sourceField, targetField))) // 字段组合规则 def combine[S](sourceFields: (S => Any)*)(targetField: String)(combiner: Seq[Any] => Any): MappingRule[S] = MappingRule(List((s => combiner(sourceFields.map(_(s))), targetField))) // 执行转换 def convert[S, T](source: S, rules: MappingRule[S]*)(implicit ct: scala.reflect.ClassTag[T]): T = { val constructor = ct.runtimeClass.getDeclaredConstructors.head val ruleMap = rules.flatMap(_.rules).toMap val targetFieldNames = ct.runtimeClass.getDeclaredFields.map(_.getName) val args = targetFieldNames.map { fieldName => ruleMap.get(fieldName) match { case Some(rule) => rule(source) case None => // 自动匹配同名字段 source.getClass.getMethod(fieldName).invoke(source) } } constructor.newInstance(args: _*).asInstanceOf[T] } } // 使用示例 // 字段名映射场景 val dto = UserDTO("John", "Doe", 30) val user1 = MappingDSL.convert(dto, MappingDSL.alias[UserDTO](_.name)("firstName") ) // 输出:User(John, Doe, 30) // 字段组合场景 case class User(fullName: String, age: Int) val user2 = MappingDSL.convert(dto, MappingDSL.combine[UserDTO](_.name, _.surname)("fullName")(_.mkString) ) // 输出:User(JohnDoe, 30)
三、Scala反射方案
反射方案实现简单,但缺少编译时类型检查,适合快速原型或规则简单的场景:
import scala.reflect.runtime.universe._ object ReflectConverter { def convert[S: TypeTag, T: TypeTag](source: S): T = { val mirror = runtimeMirror(source.getClass.getClassLoader) val sourceInstance = mirror.reflect(source) val targetClass = typeOf[T].typeSymbol.asClass val targetMirror = mirror.reflectClass(targetClass) val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod val paramNames = constructor.paramLists.head.map(_.name.toString) val args = paramNames.map { paramName => paramName match { case "firstName" => sourceInstance.reflectField(typeOf[S].decl(TermName("name")).asTerm).get case "fullName" => val name = sourceInstance.reflectField(typeOf[S].decl(TermName("name")).asTerm).get.asInstanceOf[String] val surname = sourceInstance.reflectField(typeOf[S].decl(TermName("surname")).asTerm).get.asInstanceOf[String] name + surname case _ => // 自动匹配同名字段 sourceInstance.reflectField(typeOf[S].decl(TermName(paramName)).asTerm).get }.asInstanceOf[AnyRef] } targetMirror.reflectConstructor(constructor)(args: _*).asInstanceOf[T] } } // 使用示例 val dto = UserDTO("John", "Doe", 30) val user1 = ReflectConverter.convert[UserDTO, User](dto) // 字段名映射场景 val user2 = ReflectConverter.convert[UserDTO, User](dto) // 字段组合场景
方案选型建议
- 类型安全优先:选择基于Shapeless的扩展方案,编译时即可检查字段匹配和类型正确性。
- 简洁易用优先:选择类似Quill Alias的DSL方案,语法直观,学习成本低。
- 快速实现:反射方案适合小项目或临时需求,但需注意运行时错误风险。
内容的提问来源于stack exchange,提问作者Lindenes
相关产品推荐
相关产品推荐

