如何生成复杂对象差异的JSON表示?Kotlin多层级对象对比与REST更新方案
问题1:如何创建两个复杂对象之间差异的JSON表示?
处理复杂对象的差异JSON生成,核心思路是递归遍历对象的所有属性,对比新旧值,仅保留变更的部分(包括嵌套对象内的变更)。这里分享两种实用方案:
方案一:手动递归对比(自定义逻辑灵活)
如果需要完全自定义对比规则(比如忽略某些属性、特殊处理集合),可以用Kotlin反射实现递归对比。举个简单的实现示例:
import com.google.gson.Gson import kotlin.reflect.full.declaredMemberProperties /** * 对比两个同类型对象,生成仅包含变更属性的JSON * 支持多层嵌套数据类 */ fun <T> generateDiffJson(oldObj: T, newObj: T): String { val diffMap = mutableMapOf<String, Any?>() val properties = oldObj::class.declaredMemberProperties properties.forEach { prop -> prop.isAccessible = true // 确保能访问私有属性 val oldValue = prop.getter.call(oldObj) val newValue = prop.getter.call(newObj) when { // 旧值为空,新值非空:记录新值 oldValue == null && newValue != null -> diffMap[prop.name] = newValue // 旧值非空,新值为空:记录null(如果后端支持用null删除属性) oldValue != null && newValue == null -> diffMap[prop.name] = null // 新旧值都非空且不相等:判断是否是嵌套数据类,递归处理 oldValue != null && newValue != null && oldValue != newValue -> { if (oldValue::class.isData && newValue::class == oldValue::class) { val nestedDiffJson = generateDiffJson(oldValue, newValue) // 如果嵌套对象有变更,才加入差异Map if (nestedDiffJson != "{}") { diffMap[prop.name] = Gson().fromJson(nestedDiffJson, Map::class.java) } } else { // 非嵌套类型,直接记录新值 diffMap[prop.name] = newValue } } } } return Gson().toJson(diffMap) }
优缺点
- ✅ 完全自定义对比逻辑,适合有特殊规则的场景
- ❌ 需要手动处理反射权限、集合差异(比如List/Set的元素变更)、循环引用等边界情况
方案二:使用现成的对象差异库
如果不想重复造轮子,推荐用成熟的第三方库来处理复杂对象对比,比如de.danielbechler:object-diff,它支持递归对比、集合处理、循环引用检测等。
先引入依赖(Gradle):
implementation 'de.danielbechler:object-diff:0.95'
然后实现差异生成:
import de.danielbechler.diff.ObjectDifferBuilder import de.danielbechler.diff.node.DiffNode import com.google.gson.Gson fun <T> generateDiffJsonWithLibrary(oldObj: T, newObj: T): String { val differ = ObjectDifferBuilder.buildDefault() val diffNode = differ.compare(oldObj, newObj) val diffMap = mutableMapOf<String, Any?>() // 遍历差异节点,转换为Map diffNode.visit { node, accessor -> if (node.hasChanges()) { // 用属性路径(比如address.street)作为key,适合后端支持嵌套路径的场景 val propertyPath = accessor.path.joinToString(".") diffMap[propertyPath] = node.canonicalValue } } return Gson().toJson(diffMap) }
优缺点
- ✅ 无需手动处理递归、边界情况,开箱即用
- ❌ 需要引入第三方依赖,需学习库的API来适配自定义需求
问题2:Kotlin中对比多层嵌套对象并仅发送变更属性到REST后端(Retrofit+Gson)
结合你的业务场景(多层嵌套类、Retrofit+Gson),核心流程是:保存原始对象副本 → 生成差异数据 → 通过Retrofit发送仅包含变更属性的请求。这里分步骤讲解:
步骤1:保存原始对象的深拷贝
当从后端获取到对象后,必须保存一个深拷贝的原始版本,避免后续修改影响对比结果。用Gson实现深拷贝最简单:
/** * 用Gson实现对象的深拷贝 */ fun <T> deepCopy(obj: T, gson: Gson): T { val json = gson.toJson(obj) return gson.fromJson(json, obj::class.java) } // 使用示例:获取后端数据后保存原始副本 val fetchedUser: User = userApi.getUser(userId) val originalUser = deepCopy(fetchedUser, Gson())
步骤2:生成差异数据
使用问题1中的任意一种方案,对比原始对象和修改后的对象,生成差异Map或JSON字符串。推荐用Map格式,因为Retrofit+Gson可以直接将Map序列化为JSON。
步骤3:定义Retrofit接口并发送请求
接口定义时,用Map<String, Any?>作为请求体,Gson会自动将其序列化为仅包含变更属性的JSON:
interface UserApi { // PATCH方法适合部分更新场景,后端需支持接收部分字段 @PATCH("/users/{id}") suspend fun updateUser( @Path("id") userId: String, @Body diff: Map<String, Any?> ): Response<Unit> } // 使用示例:发送差异请求 val modifiedUser: User = ... // 用户修改后的对象 val diffJson = generateDiffJson(originalUser, modifiedUser) val diffMap = Gson().fromJson(diffJson, Map::class.java) userApi.updateUser(userId, diffMap)
进阶方案:基于变更标记的序列化(性能更优)
如果不想做全量对象对比,可以给每个属性添加变更标记,修改属性时自动标记,然后让Gson仅序列化已变更的属性。适合频繁修改、性能要求高的场景:
1. 修改数据类,添加变更标记
data class User( val id: String, private var _name: String, private var _address: Address ) { // 变更标记 var isNameChanged = false var isAddressChanged = false // 重写setter,修改时标记变更 var name: String get() = _name set(value) { if (_name != value) { _name = value isNameChanged = true } } var address: Address get() = _address set(value) { if (_address != value) { _address = value isAddressChanged = true } } }
2. 自定义Gson排除策略
class ChangedPropertyExclusionStrategy : ExclusionStrategy { override fun shouldSkipField(f: FieldAttributes): Boolean { // 跳过变更标记字段 if (f.name.startsWith("is")) return true // 检查对应属性的变更标记 val changedFieldName = "is${f.name.replaceFirstChar { it.uppercase() }}Changed" return try { val changedField = f.declaringClass.getDeclaredField(changedFieldName) changedField.isAccessible = true !(changedField.get(f.declaringClass) as Boolean) } catch (e: NoSuchFieldException) { false // 没有标记的字段默认不跳过 } } override fun shouldSkipClass(clazz: Class<*>?): Boolean = false }
3. 配置Gson并发送请求
// 创建带自定义策略的Gson实例 val gson = GsonBuilder() .setExclusionStrategies(ChangedPropertyExclusionStrategy()) .create() // Retrofit接口用原始对象作为请求体,Gson会自动只序列化已变更的属性 interface UserApi { @PATCH("/users/{id}") suspend fun updateUser( @Path("id") userId: String, @Body user: User ): Response<Unit> } // 使用示例:直接发送修改后的对象即可 userApi.updateUser(userId, modifiedUser)
注意事项
- 集合差异处理:如果对象包含List/Set,直接用
equals对比可能不准确(比如顺序不同但元素相同),可以自定义集合对比逻辑,或者用库的集合差异检测功能。 - 空值处理:确认后端是否接受
null值来表示删除属性,如果不接受,需要调整差异生成逻辑(比如不包含null值,或用特殊标记)。 - 循环引用:如果对象存在循环引用,手动递归对比会导致栈溢出,建议用第三方库处理。
内容的提问来源于stack exchange,提问作者tenprint
相关产品推荐
相关产品推荐

