Kotlin如何实现类对外可见但构造函数仅模块内可访问
问题原因
- Kotlin的internal是编译期生效的模块级访问限制,JVM字节码规范没有对应的原生访问修饰符,默认仅在Kotlin代码跨模块编译时做访问校验,不会在字节码层面直接阻止调用。
- 给带默认参数的构造函数加internal修饰时,Kotlin为了实现默认参数逻辑自动生成的合成构造、无参重载构造不会同步继承internal可见性,会被生成为public,直接导致外部代码(尤其是Java代码)可以绕过Kotlin编译器检查直接实例化类。
可行实现方案
方案1:私有主构造 + 模块内可见创建入口(最可靠,无可见性泄漏)
将类的主构造声明为private,从字节码层面彻底禁止外部直接调用构造实例,再在模块内提供构造入口,既可以保留直接构造的调用体验,也可以用工厂方法实现。
1.1 保留原生构造调用体验
通过给私有主构造增加一个无意义的私有标记参数避免签名冲突,再编写对应签名的internal次级构造供模块内调用,如果需要阻止Java代码绕过Kotlin编译器访问,给次级构造加上*@JvmSynthetic*注解即可:
class UserInformationException private constructor( msg: String, // 仅用于区分签名的私有标记参数,外部无法传值 private val marker: Nothing? ) : Exception(msg) { // 模块内可直接调用的带自定义消息构造 @JvmSynthetic internal constructor(msg: String) : this(msg, null) // 模块内可直接调用的默认消息构造 @JvmSynthetic internal constructor() : this(ERROR_MESSAGE, null) companion object { private const val ERROR_MESSAGE = "User information wrong!!" } }
模块内调用方式和普通类完全一致:
val e1 = UserInformationException() val e2 = UserInformationException("自定义错误信息")
1.2 工厂方法写法
如果不介意调用方式稍作调整,可以直接在伴生对象里定义internal修饰的工厂方法,代码更简洁,不存在签名冲突问题:
class UserInformationException private constructor(msg: String) : Exception(msg) { companion object { private const val ERROR_MESSAGE = "User information wrong!!" @JvmSynthetic internal fun create(msg: String = ERROR_MESSAGE): UserInformationException { return UserInformationException(msg) } } }
模块内调用:
val e1 = UserInformationException.create() val e2 = UserInformationException.create("自定义错误信息")
方案2:显式声明所有构造重载 + @JvmSynthetic注解
如果不想修改构造的调用体验,也不想加私有标记参数,就不要在主构造上使用默认参数,显式写出所有重载的构造函数,给每个构造都加上internal修饰和*@JvmSynthetic*注解:
class UserInformationException @JvmSynthetic internal constructor(msg: String) : Exception(msg) { @JvmSynthetic internal constructor() : this(ERROR_MESSAGE) companion object { private const val ERROR_MESSAGE = "User information wrong!!" } }
注意:这种方案绝对不能在构造参数上使用默认值,否则Kotlin自动生成的合成默认参数构造不会被加上*@JvmSynthetic*和internal限制,仍然会出现public构造泄漏的问题。
方案选择建议
- 如果追求绝对的访问限制,完全杜绝外部(反射除外)实例化的可能,优先选方案1的私有主构造实现,JVM层面的private修饰是强约束,不会随Kotlin编译器版本变化出现可见性漏洞。
- 如果类构造逻辑简单、重载数量少,选方案2即可,注意不要使用默认参数,显式声明所有构造重载。
内容的提问来源于stack exchange,提问作者lannyf
相关产品推荐
相关产品推荐

