KMP类GSON转字符串工具

当前场景,主要是为了联调,数据结构可能复杂嵌套,不可修改,没有@Serializable之类的注解,需要看到数据的json格式打印。

完善的调试打印工具 (DebugUtils)

以下是一个功能完善的调试工具,专门用于处理复杂嵌套数据结构的 JSON 格式打印:

kotlin 复制代码
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

object DebugUtils {
    
    // 配置选项
    data class DebugConfig(
        val maxDepth: Int = 10,                    // 最大嵌套深度
        val maxCollectionSize: Int = 100,          // 集合最大显示元素数量
        val maxStringLength: Int = 200,            // 字符串最大长度
        val showNullValues: Boolean = true,        // 是否显示 null 值
        val showSizeForCollections: Boolean = true, // 是否显示集合大小
        val prettyPrint: Boolean = true,           // 是否格式化输出
        val indentSpaces: Int = 2,                // 缩进空格数
        val excludeClasses: Set<KClass<*>> = setOf(), // 排除的类(避免循环引用)
        val includePrivateFields: Boolean = false  // 是否包含私有字段
    )

    // 默认配置
    private val defaultConfig = DebugConfig()

    /**
     * 将任意对象转换为格式化的 JSON 字符串(用于调试)
     */
    inline fun <reified T : Any> T.toDebugJson(
        config: DebugConfig = defaultConfig
    ): String {
        return buildDebugJson(this, config, 0, mutableSetOf())
    }

    /**
     * 打印对象的调试信息
     */
    inline fun <reified T : Any> T.printDebugJson(
        tag: String? = null,
        config: DebugConfig = defaultConfig
    ) {
        val jsonString = toDebugJson(config)
        val prefix = tag?.let { "[$it] " } ?: ""
        println("$prefix${this::class.simpleName ?: "Unknown"}: $jsonString")
    }

    /**
     * 批量打印列表中的对象
     */
    inline fun <reified T : Any> Collection<T>.printAllDebugJson(
        tag: String? = null,
        config: DebugConfig = defaultConfig.copy(maxCollectionSize = 10) // 限制批量打印数量
    ) {
        val prefix = tag?.let { "[$it] " } ?: ""
        take(config.maxCollectionSize).forEachIndexed { index, item ->
            val itemJson = item.toDebugJson(config)
            println("${prefix}${item::class.simpleName}[$index]: $itemJson")
        }
        if (size > config.maxCollectionSize) {
            println("${prefix}... and ${size - config.maxCollectionSize} more items")
        }
    }

    // 内部实现
    private fun buildDebugJson(
        obj: Any?,
        config: DebugConfig,
        currentDepth: Int,
        seenObjects: MutableSet<Int>
    ): String {
        return when {
            obj == null -> "null"
            currentDepth >= config.maxDepth -> ""<max depth reached>""
            isPrimitiveType(obj) -> formatPrimitive(obj, config)
            isString(obj) -> formatString(obj.toString(), config)
            isCollection(obj) -> formatCollection(obj, config, currentDepth, seenObjects)
            isMap(obj) -> formatMap(obj, config, currentDepth, seenObjects)
            isArray(obj) -> formatArray(obj, config, currentDepth, seenObjects)
            else -> formatObject(obj, config, currentDepth, seenObjects)
        }
    }

    private fun isPrimitiveType(obj: Any): Boolean {
        return obj is Number || obj is Boolean || obj is Char
    }

    private fun isString(obj: Any): Boolean {
        return obj is String
    }

    private fun isCollection(obj: Any): Boolean {
        return obj is Collection<*>
    }

    private fun isMap(obj: Any): Boolean {
        return obj is Map<*, *>
    }

    private fun isArray(obj: Any): Boolean {
        return obj::class.java.isArray
    }

    private fun formatPrimitive(obj: Any, config: DebugConfig): String {
        return when (obj) {
            is String -> formatString(obj, config) // 这个应该不会走到,但保留安全
            is Char -> ""$obj""
            else -> obj.toString()
        }
    }

    private fun formatString(str: String, config: DebugConfig): String {
        val truncated = if (str.length > config.maxStringLength) {
            str.substring(0, config.maxStringLength) + "..."
        } else {
            str
        }
        
        // 转义特殊字符
        val escaped = truncated
            .replace("\", "\\")
            .replace(""", "\"")
            .replace("\n", "\n")
            .replace("\r", "\r")
            .replace("\t", "\t")
        
        return ""$escaped""
    }

    private fun formatCollection(
        collection: Any,
        config: DebugConfig,
        currentDepth: Int,
        seenObjects: MutableSet<Int>
    ): String {
        val col = collection as Collection<*>
        
        if (col.isEmpty()) return "[]"
        
        val sizeInfo = if (config.showSizeForCollections) " // size=${col.size}" else ""
        
        val items = col.take(config.maxCollectionSize).joinToString(",") { item ->
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            val itemJson = buildDebugJson(item, config, currentDepth + 1, seenObjects)
            if (config.prettyPrint) "$indent$itemJson" else itemJson
        }
        
        val remaining = if (col.size > config.maxCollectionSize) {
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            "${if (config.prettyPrint) "," else ""}$indent"... and ${col.size - config.maxCollectionSize} more""
        } else ""
        
        val endIndent = if (config.prettyPrint) "\n${" ".repeat(currentDepth * config.indentSpaces)}" else ""
        
        return "[$items$remaining$endIndent]$sizeInfo"
    }

    private fun formatMap(
        map: Any,
        config: DebugConfig,
        currentDepth: Int,
        seenObjects: MutableSet<Int>
    ): String {
        val m = map as Map<*, *>
        
        if (m.isEmpty()) return "{}"
        
        val sizeInfo = if (config.showSizeForCollections) " // size=${m.size}" else ""
        
        val entries = m.entries.take(config.maxCollectionSize).joinToString(",") { (key, value) ->
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            val keyJson = buildDebugJson(key, config, currentDepth + 1, seenObjects)
            val valueJson = buildDebugJson(value, config, currentDepth + 1, seenObjects)
            if (config.prettyPrint) "$indent$keyJson: $valueJson" else "$keyJson:$valueJson"
        }
        
        val remaining = if (m.size > config.maxCollectionSize) {
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            "${if (config.prettyPrint) "," else ""}$indent"...": "${m.size - config.maxCollectionSize} more entries""
        } else ""
        
        val endIndent = if (config.prettyPrint) "\n${" ".repeat(currentDepth * config.indentSpaces)}" else ""
        
        return "{$entries$remaining$endIndent}$sizeInfo"
    }

    private fun formatArray(
        array: Any,
        config: DebugConfig,
        currentDepth: Int,
        seenObjects: MutableSet<Int>
    ): String {
        val size = java.lang.reflect.Array.getLength(array)
        
        if (size == 0) return "[]"
        
        val items = (0 until minOf(size, config.maxCollectionSize)).joinToString(",") { index ->
            val item = java.lang.reflect.Array.get(array, index)
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            val itemJson = buildDebugJson(item, config, currentDepth + 1, seenObjects)
            if (config.prettyPrint) "$indent$itemJson" else itemJson
        }
        
        val remaining = if (size > config.maxCollectionSize) {
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            "${if (config.prettyPrint) "," else ""}$indent"... and ${size - config.maxCollectionSize} more""
        } else ""
        
        val endIndent = if (config.prettyPrint) "\n${" ".repeat(currentDepth * config.indentSpaces)}" else ""
        
        return "[$items$remaining$endIndent]"
    }

    private fun formatObject(
        obj: Any,
        config: DebugConfig,
        currentDepth: Int,
        seenObjects: MutableSet<Int>
    ): String {
        // 防止循环引用
        val identityHashCode = System.identityHashCode(obj)
        if (identityHashCode in seenObjects) {
            return ""<cyclic reference to ${obj::class.simpleName}>""
        }
        seenObjects.add(identityHashCode)
        
        // 检查排除的类
        if (obj::class in config.excludeClasses) {
            return ""<excluded class: ${obj::class.simpleName}>""
        }
        
        val properties = try {
            obj::class.memberProperties
                .filter { config.includePrivateFields || it.visibility != null }
                .mapNotNull { prop ->
                    try {
                        // 确保可以访问私有属性
                        if (!prop.isAccessible) {
                            prop.isAccessible = true
                        }
                        prop to prop.get(obj)
                    } catch (e: Exception) {
                        null // 忽略无法访问的属性
                    }
                }
                .filter { (_, value) -> config.showNullValues || value != null }
        } catch (e: Exception) {
            emptyList() // 如果反射失败,返回空列表
        }
        
        if (properties.isEmpty()) {
            return "{}"
        }
        
        val propsString = properties.joinToString(",") { (prop, value) ->
            val indent = if (config.prettyPrint) "\n${" ".repeat((currentDepth + 1) * config.indentSpaces)}" else " "
            val valueJson = buildDebugJson(value, config, currentDepth + 1, seenObjects)
            if (config.prettyPrint) "$indent"${prop.name}": $valueJson" else ""${prop.name}":$valueJson"
        }
        
        val endIndent = if (config.prettyPrint) "\n${" ".repeat(currentDepth * config.indentSpaces)}" else ""
        
        return "{$propsString$endIndent}"
    }

    // 便捷的配置构建器
    fun debugConfig(block: DebugConfigBuilder.() -> Unit): DebugConfig {
        val builder = DebugConfigBuilder()
        builder.block()
        return builder.build()
    }

    class DebugConfigBuilder {
        var maxDepth: Int = 10
        var maxCollectionSize: Int = 100
        var maxStringLength: Int = 200
        var showNullValues: Boolean = true
        var showSizeForCollections: Boolean = true
        var prettyPrint: Boolean = true
        var indentSpaces: Int = 2
        var excludeClasses: MutableSet<KClass<*>> = mutableSetOf()
        var includePrivateFields: Boolean = false

        fun build(): DebugConfig {
            return DebugConfig(
                maxDepth = maxDepth,
                maxCollectionSize = maxCollectionSize,
                maxStringLength = maxStringLength,
                showNullValues = showNullValues,
                showSizeForCollections = showSizeForCollections,
                prettyPrint = prettyPrint,
                indentSpaces = indentSpaces,
                excludeClasses = excludeClasses,
                includePrivateFields = includePrivateFields
            )
        }
    }
}

// 扩展函数,便于使用
inline fun <reified T : Any> T.debugJson(): String = DebugUtils.toDebugJson(this)

inline fun <reified T : Any> T.printDebug(tag: String? = null) = 
    DebugUtils.printDebugJson(this, tag)

inline fun <reified T : Any> Collection<T>.printAllDebug(tag: String? = null) = 
    DebugUtils.printAllDebugJson(this, tag)

使用示例

kotlin 复制代码
// 示例数据类(假设这些是第三方库的类,不能修改)
data class User(
    val id: Int,
    val name: String,
    private val password: String, // 私有字段
    val email: String? = null,    // 可空字段
    val profile: Profile,
    val tags: List<String> = emptyList(),
    val preferences: Map<String, Any> = emptyMap()
)

data class Profile(
    val age: Int,
    val address: Address,
    val friends: List<User> = emptyList() // 循环引用测试
)

data class Address(
    val street: String,
    val city: String,
    val coordinates: DoubleArray
)

// 使用示例
fun main() {
    val address = Address("Main St", "New York", doubleArrayOf(40.7128, -74.0060))
    val profile = Profile(30, address)
    val user = User(
        id = 1,
        name = "John Doe",
        password = "secret",
        email = "john@example.com",
        profile = profile,
        tags = listOf("android", "kotlin", "kmp"),
        preferences = mapOf(
            "theme" to "dark",
            "notifications" to true,
            "language" to "en"
        )
    )
    
    // 基本使用
    user.printDebug()
    
    // 带标签打印
    user.printDebug("UserService")
    
    // 自定义配置
    val customConfig = DebugUtils.debugConfig {
        maxDepth = 5
        maxCollectionSize = 50
        showNullValues = false
        includePrivateFields = true // 显示私有字段
    }
    
    val json = user.toDebugJson(customConfig)
    println("Custom: $json")
    
    // 批量打印
    val users = listOf(user, user, user)
    users.printAllDebug("UserList")
    
    // 处理复杂嵌套和循环引用
    val complexConfig = DebugUtils.debugConfig {
        maxDepth = 3
        excludeClasses = setOf(User::class) // 避免循环引用
    }
    
    user.printDebug(complexConfig)
}

主要特性

  1. 深度嵌套处理 - 支持任意深度的对象嵌套
  2. 循环引用检测 - 自动检测并避免无限递归
  3. 性能控制 - 可配置最大深度和集合大小
  4. 私有字段访问 - 可选是否显示私有字段
  5. 格式化输出 - 可读性强的缩进格式
  6. 类型安全 - 完整的类型处理
  7. 错误处理 - 优雅处理反射异常
  8. 批量操作 - 支持集合的批量打印

这个工具非常适合调试复杂的第三方数据结构,无需修改原始类即可获得详细的 JSON 格式输出。

相关推荐
CoderJia程序员甲3 小时前
GitHub 热榜项目 - 日榜(2025-11-06)
ai·开源·大模型·github·ai教程
诸葛思颖6 小时前
把本地 Python 项目用 Git 进行版本控制并推送到 GitHub
git·python·github
chhanz17 小时前
git/github入门基操(终端版)
git·github
Bacon20 小时前
Electron 应用商店:开箱即用工具集成方案
前端·github
掘金安东尼1 天前
GPT-6 会带来科学革命?奥特曼最新设想:AI CEO、便宜医疗与全新计算机
前端·vue.js·github
逛逛GitHub1 天前
国产开源 AI CRM 系统,用它替换了 7 年的 Salesforce。
github
散峰而望1 天前
C++入门(二) (算法竞赛)
开发语言·c++·算法·github
HelloGitHub1 天前
让 AI 记住我家狗叫「十六」,原来只需要 5 分钟
开源·github
CoderJia程序员甲1 天前
GitHub 热榜项目 - 日榜(2025-11-04)
开源·github·ai编程·github热榜