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

如何生成复杂对象差异的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:16:18