迎接 Kotlin + JPA 中的"默认构造函数地狱"
\n\n如果你曾尝试让 Kotlin 与 JPA(Java 持久化 API) 协同工作,很可能遇到过 Hibernate 在启动应用程序时抛出类似这样的异常:
\n\norg.hibernate.InstantiationException: No default constructor for entity: com.example.User\n\n\n听起来很熟悉吧?确实如此。这个问题在 Stack Overflow 上积累了成千上万的浏览量,并被冠以了可怕的名称——"Kotlin with JPA: default constructor hell"。
\n\n在这篇文章中,我们将冷静地、一步一步地拆解:
\n- \n
- 这个错误最初发生的原因 \n
- 为什么 Kotlin "不愿意"生成空构造函数 \n
- 三种经过验证的解决问题的方法(从简单到专业) \n
- 为你的项目选择哪种方法 \n
我们开始吧!
\n\n为什么 Hibernate 需要无参构造函数?
\n\n让我们从 Java 说起。在经典的 Java 中,我们习惯这样写:
\n\npublic class User {\n private Long id;\n private String name;\n\n // 必须的空构造函数\n public User() {}\n\n // Getters, setters...\n}\n\n\nHibernate(以及任何 JPA 实现)使用反射来创建对象。当它从数据库中读取一行时,需要创建一个 User 类的实例,然后通过反射填充字段。为此,它需要一个 无参构造函数(默认构造函数)。
在 Java 中,如果你没有定义任何其他构造函数,编译器会自动创建一个空构造函数。因此,通常不会出现这个问题。
\n\nKotlin 有什么特别之处?
\n\nKotlin 是一种非常强调不可变性和安全性的语言。看看一个典型的 Kotlin 类:
\n\ndata class User(\n val id: Long = 0,\n val name: String = ""\n)\n\n\n这里发生了两件关键的事情:
\n- \n
- 所有属性都在 主构造函数 中声明 \n
- 属性被声明为
val(不可变)并具有默认值 \n
如果主构造函数有参数,Kotlin 不会生成一个单独的空构造函数。即使所有参数都有默认值,在 JVM 字节码层面,这也不被视为一个"真正的"无参构造函数。
\n\n当 Hibernate 试图通过反射调用 User.class.newInstance() 时,它找不到空构造函数——然后就会崩溃并报错。
方法一:最简单——显式添加一个次构造函数
\n\n最直接的方法是手动添加第二个空构造函数:
\n\ndata class User(\n val id: Long = 0,\n val name: String = ""\n) {\n // 无参次构造函数\n constructor() : this()\n}\n\n\n这里,constructor() : this() 的意思是:"使用默认值调用主构造函数"。
优点:
\n- \n
- 非常简单明了 \n
- 不需要插件或额外配置 \n
缺点:
\n- \n
- 你需要为每个实体编写这个构造函数 \n
- 如果你有 10-15 个实体,这会变得很繁琐 \n
- 容易忘记添加 \n
方法二:使用 open 类并将所有属性设为 var
\n\n网上很多例子建议使用这个解决方案:
\n\nopen class User {\n var id: Long = 0\n var name: String = ""\n}\n\n\n这里,我们将类设为 open(这样 Hibernate 就能创建代理),并将属性设为 var(可变的)。而且没有主构造函数——这意味着 Kotlin 会生成一个默认的空构造函数。
优点:
\n- \n
- 非常简单且无需额外样板代码 \n
- 完全符合 JPA 要求 \n