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

如何让数据类仅复制已初始化属性以避免StackOverflowError?

数据类toString()掩码敏感字段时避免栈溢出的方案

问题回顾

我有个包含大量属性的数据类,其中email和phoneNumber是敏感字段,日志输出时得做掩码处理,已经实现了String.maskEmail()和String.maskPhoneNumber()这两个扩展方法。但最初的写法直接触发了StackOverflowError,代码如下:

data class Foo(
    val email: String = "email",
    val phoneNumber: String = "phoneNumber",
    // 实际还有几十个其他属性
) {
    override fun toString() = withMaskedProps

    private val withMaskedProps: String = copy(
        email = email.maskEmail(), phoneNumber = phoneNumber.maskPhoneNumber(),
    ).toString()
}

原因很明显:初始化withMaskedProps时调用了copy().toString(),而重写的toString()又返回withMaskedProps,直接形成递归调用,栈被撑爆了。

可行解决方案

方案一:手动拼接掩码字符串(简单直接)

直接访问每个属性,手动拼出toString()的内容,彻底避开递归调用。适合字段变动不频繁的场景:

data class Foo(
    val email: String = "email",
    val phoneNumber: String = "phoneNumber",
    val name: String = "name",
    val age: Int = 18
    // 其他属性
) {
    override fun toString(): String {
        return "Foo(email=${email.maskEmail()}, phoneNumber=${phoneNumber.maskPhoneNumber()}, name=$name, age=$age)"
    }
}

缺点:新增属性时必须手动更新toString()的拼接逻辑,不符合“新增属性不用维护”的需求。

方案二:反射+注解实现通用掩码(最推荐)

用反射遍历数据类的所有属性,给敏感字段加个注解标记,toString时自动处理掩码。新增敏感字段只需要加注解,完全不用改toString逻辑,完美满足通用需求:

先定义一个敏感字段注解:

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Sensitive

然后修改数据类,给敏感字段打上注解:

data class Foo(
    @Sensitive val email: String = "email",
    @Sensitive val phoneNumber: String = "phoneNumber",
    val name: String = "name",
    val age: Int = 18
    // 其他属性随便加,不用管toString
) {
    override fun toString(): String {
        val propertyStrings = this::class.memberProperties
            .map { prop ->
                val rawValue = prop.get(this)
                val finalValue = if (prop.isAnnotationPresent(Sensitive::class)) {
                    // 根据字段名匹配对应的掩码方法
                    when (prop.name) {
                        "email" -> (rawValue as String).maskEmail()
                        "phoneNumber" -> (rawValue as String).maskPhoneNumber()
                        // 以后加新的敏感字段,在这加分支就行
                        else -> rawValue
                    }
                } else {
                    rawValue
                }
                "${prop.name}=$finalValue"
            }
            .joinToString(", ")
        return "${this::class.simpleName}($propertyStrings)"
    }
}

优点:新增敏感字段只需要加@Sensitive注解,非敏感字段直接加就行,完全不用维护toString逻辑;而且不需要复制对象,从根源避免了递归问题。

方案三:调用数据类默认的toString实现(保留默认格式)

如果想保留Kotlin数据类默认的toString格式,又不想手动拼接,可以通过反射调用编译器生成的原始toString方法,绕过我们重写的版本:

data class Foo(
    val email: String = "email",
    val phoneNumber: String = "phoneNumber",
    val name: String = "name"
) {
    override fun toString(): String {
        // 先创建掩码后的副本
        val maskedCopy = copy(
            email = email.maskEmail(),
            phoneNumber = phoneNumber.maskPhoneNumber()
        )
        // 反射拿到编译器生成的原始toString方法,绕过重写的版本
        val defaultToString = maskedCopy::class.java.getDeclaredMethod("toString")
        defaultToString.isAccessible = true
        return defaultToString.invoke(maskedCopy) as String
    }
}

注意:这种方式依赖Kotlin编译器的生成逻辑,虽然能工作,但灵活性不如方案二,而且如果数据类有继承关系可能会出问题。

总结

  • 追求简单直接、字段变动少:选方案一
  • 要通用可维护、新增属性不用管:选方案二(最推荐)
  • 想保留数据类默认toString格式:选方案三

内容的提问来源于stack exchange,提问作者user25583367

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:55:11