如何让数据类仅复制已初始化属性以避免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
相关产品推荐
相关产品推荐

