英文:
Sealed class as Entity
问题
I have an issue with sealed classes. If I run my application from docker it works perfectly fine, but if I do the same in IntelliJ I run into the following exception:
java.lang.IncompatibleClassChangeError: class com.nemethlegtechnika.products.model.Attribute$HibernateProxy$D0WxdNVz cannot inherit from sealed class com.nemethlegtechnika.products.model.Attribute
If I use an abstract class instead of a sealed one, I get no errors in IntelliJ as well as in Docker. Can you guys help me find the root of the problem?
英文:
I have an issue with sealed classes. If I run my application from docker it works perfectly fine, but if I do the same in IntelliJ I run into the following exception:
java.lang.IncompatibleClassChangeError: class com.nemethlegtechnika.products.model.Attribute$HibernateProxy$D0WxdNVz cannot inherit from sealed class com.nemethlegtechnika.products.model.Attribute
If I use an abstract class instead of a sealed one, I get no errors in IntelliJ as well as in Docker. Can you guys help me find the root of the problem?
Thanks in advance and have a wonderfull day!
The Classes:
package com.nemethlegtechnika.products.model
import jakarta.persistence.*
import org.hibernate.annotations.DiscriminatorFormula
@Entity
@Table(name = "attribute")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when stringValues is not null then 'string' else 'boolean' end")
sealed class Attribute : BaseEntity() {
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "product_id")
val product: Product? = null
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "group_id")
val group: Group? = null
abstract val value: Any
}
@Entity
@DiscriminatorValue("boolean")
class BooleanAttribute : Attribute() {
@Column(name = "boolean_value", nullable = true)
val booleanValue: Boolean = false
override val value: Boolean
get() = booleanValue
}
@Entity
@DiscriminatorValue("string")
class StringAttribute : Attribute() {
@Column(name = "string_value", nullable = true)
val stringValue: String = ""
override val value: String
get() = stringValue
}
The Error:
Caused by: java.lang.IncompatibleClassChangeError: class com.nemethlegtechnika.products.model.Attribute$HibernateProxy$D0WxdNVz cannot inherit from sealed class com.nemethlegtechnika.products.model.Attribute
at java.base/java.lang.ClassLoader.defineClass0(Native Method) ~[na:na]
at java.base/java.lang.System$2.defineClass(System.java:2307) ~[na:na]
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439) ~[na:na]
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2416) ~[na:na]
at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at net.bytebuddy.utility.Invoker$Dispatcher.invoke(Unknown Source) ~[na:na]
at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1032) ~[byte-buddy-1.12.23.jar:na]
at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1162) ~[byte-buddy-1.12.23.jar:na]
at jdk.proxy2/jdk.proxy2.$Proxy118.defineClass(Unknown Source) ~[na:na]
at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1638) ~[byte-buddy-1.12.23.jar:na]
at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:118) ~[byte-buddy-1.12.23.jar:na]
at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$UsingLookup.load(ClassLoadingStrategy.java:519) ~[byte-buddy-1.12.23.jar:na]
at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:101) ~[byte-buddy-1.12.23.jar:na]
at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6317) ~[byte-buddy-1.12.23.jar:na]
at org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState$1.run(ByteBuddyState.java:203) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
at org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState$1.run(ByteBuddyState.java:199) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
at org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState.lambda$load$0(ByteBuddyState.java:212) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168) ~[byte-buddy-1.12.23.jar:na]
答案1
得分: 3
这看起来像是Kotlin的密封类和Hibernate的代理创建之间的冲突。
- Kotlin中的密封类有一个限制,阻止任何其他类在其文件之外(因此在其控制之外)继承它们。
- 然而,Hibernate通常使用代理类进行延迟加载,这本质上是目标类的子类。
当您在IntelliJ中运行此代码时,Hibernate试图创建Attribute
的代理子类。由于Attribute
是一个密封类,Kotlin拒绝了这种子类化,因此出现了IncompatibleClassChangeError
错误。
您在Docker中看不到此错误的原因可能是因为本地设置和Docker设置之间使用了不同的配置或库版本。例如,在Docker设置中,可能不使用延迟加载,或者可能有不同版本的Hibernate或Kotlin,其行为不同。
请参阅“java.lang.IncompatibleClassChangeError
”以及其中的内容:
> 我认为您正在使用不兼容的Hibernate核心和注解版本。
首先,确保您的开发环境(IntelliJ)尽可能与生产环境(在本例中是Docker)保持一致。这可能包括匹配的Java、Kotlin和库版本,并确保任何JVM参数或配置是一致的。
其次,如果问题仍然存在,请尝试禁用Attribute
的延迟加载。这将阻止Hibernate创建代理子类,从而避免此错误。您可以尝试在实体上使用@Proxy(lazy = false)
注解来禁用对该特定实体的代理实例的创建。
@Entity
@Table(name = "attribute")
@Proxy(lazy = false)
sealed class Attribute : BaseEntity() {
// ...
}
> 我忘了添加,我在gradle中配置了所有带有Entity或MappedSuperclass注解的类默认都是开放的
链接“为什么JPA中的实体类不能是final?”强调了在使用JPA(Java Persistence API)提供程序时的一个关键问题,例如Hibernate。这些提供程序通常会创建实体类的运行时代理(子类)以启用诸如延迟加载之类的功能。如果一个类被标记为final
,它就无法被子类化,因此Hibernate无法创建其代理。这会导致应用程序运行时出现错误。
Kotlin类默认情况下是final
的。在Kotlin世界中,解决Hibernate代理问题的常见方法是使用kotlin-allopen
Gradle插件。该插件在编译期间将指定的类(例如那些带有@Entity
或@MappedSuperclass
注解的类)设置为非final,允许Hibernate为其创建子类以进行代理创建。
但是,这里的问题不是关于final
类,而是关于sealed
类。Kotlin中的密封类限制了可以继承它们的类。这意味着即使该类本身不是final,Hibernate也无法创建代理子类,因为它不会是密封类定义允许的预定义子类之一。
因此,解决方案将是在与JPA提供程序(如Hibernate)一起使用实体时不使用密封类,或找到一种方法来配置Hibernate不使用代理(如禁用延迟加载)这些实体,如上所述。
Chris Hinshaw在评论中提出了一个解决方法,作为一种解决方法,可以使用数据类(例如data class Attribute
),尽管它不能完全消除继承,但可以保持合同的整洁。
我曾提出,这将自动生成Kotlin的数据类方法,如equals()
、hashCode()
和copy()
,这些方法可能无法与JPA的延迟加载或代理机制正确配合使用。
但是,Chris确认:
> 我们目前使用Spring和R2DBC(Reactive Relational Database Connectivity),它应该起到相同的作用。很可能需要Kotlin / All-open编译器插件来覆盖Kotlin类的final性。
>
> 我曾经在hibernate-core中看到代理代码,它有一个过滤器来跳过equals()
和hashcode()
函数,不确定它是否有一个用于Kotlin的copy
函数的过滤器,但我认为它应该按预期工作。
> 实际上,在考虑这个问题后,“All Open Plugin”似乎也适用于密封类。将它
英文:
That looks like a conflict between Kotlin's sealed classes and Hibernate's proxy creation.
- Sealed classes in Kotlin have a restriction that prevents any other classes outside their file (and hence outside their control) from inheriting from them.
- Hibernate, however, often uses proxy classes for lazy loading, which are essentially subclasses of the target class.
When you run this code with Hibernate in IntelliJ, Hibernate is trying to create a proxy subclass of Attribute
. Since Attribute
is a sealed class, Kotlin denies this subclassing, hence the IncompatibleClassChangeError
error.
The reason you might not see this in Docker could be due to different configurations or versions of libraries being used between your local setup and your Docker setup. For example, perhaps in the Docker setup, lazy loading is not being used, or maybe there is a different version of Hibernate or Kotlin that behaves differently.
See for instance "java.lang.IncompatibleClassChangeError
", which states:
> I think you are using incompatible versions of hibernate core and annotations.
First, make sure your development environment (IntelliJ) is as close as possible to your production environment (Docker, in this case). That might include matching Java, Kotlin, and library versions and ensuring any JVM arguments or configurations are consistent.
Second, for testing, if the issue still persists, try and disable lazy loading for Attribute``. That will prevent Hibernate from creating proxy subclasses, and thus you will avoid this error.
@Proxy(lazy = false)` annotation on your entity to disable the creation of proxy instances for that specific entity.
You can try using the
@Entity
@Table(name = "attribute")
@Proxy(lazy = false)
sealed class Attribute : BaseEntity() {
// ...
}
> I forgot to add that I configured in gradle that all my classes with Entity or MappedSuperclass annotation is open by default
The "Why can't entity class in JPA be final?" link highlights a key issue when working with JPA (Java Persistence API) providers, like Hibernate. These providers often create runtime proxies (subclasses) of entity classes to enable features like lazy loading. If a class is marked as final
, it cannot be subclassed, and hence, Hibernate cannot create its proxies. That leads to errors when the application runs.
Kotlin classes are final
by default. In the Kotlin world, a common workaround for the Hibernate proxy issue is to use the kotlin-allopen
Gradle plugin. That plugin makes specified classes (e.g., those annotated with @Entity
or @MappedSuperclass
) non-final during compilation, allowing Hibernate to subclass them for proxy creation.
So, if the OP has configured their Gradle build with kotlin-allopen
to treat classes annotated with @Entity
or @MappedSuperclass
as open, then those classes should be able to be proxied by Hibernate.
However, the problem here is not with a final
class but with a sealed
class. Sealed classes in Kotlin, by design, restrict which classes can inherit from them. That means that even if the class itself is not final, Hibernate cannot create a proxy subclass because it will not be one of the pre-defined subclasses allowed by the sealed class definition.
Therefore, the solution would be to not use sealed classes for entities when working with JPA providers like Hibernate, or find a way to configure Hibernate not to use proxying (like disabling lazy loading) for these entities, as mentioned above.
Chris Hinshaw suggests in the comments, as a workaround, to use Data class (as in data class Attribute
) which, while not eliminating inheritance, will keep the contract tidy.
I objected it would bring Kotlin's data classes automatically generated methods like equals()
, hashCode()
, and copy()
, generated methods which might not behave correctly with JPA's lazy loading or proxying mechanisms.
But Chris confirmed:
> We currently use Spring and R2DBC (Reactive Relational Database Connectivity), which should function the same. It will likely require the Kotlin / All-open compiler plugin to override the finality of Kotlin classes.
>
> I have seen the proxy code in hibernate-core at one point and it has a filter to skip equals()
and hashcode()
functions, not sure if they have a filter for Kotlin's copy
function but I would imagine it would work as expected.
(possible code: hibernate/hibernate-orm
hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java
)
> Actually after thinking about it the "All Open Plugin", looks like it works for sealed classes as well. It's worth a try adding it to your plugins to check.
答案2
得分: 1
以下是您要翻译的内容:
"我不是Hibernate的专家,但我懂Kotlin,请查看以下代码部分:
sealed class Test
class Test1 : Test()
class Test2 : Test()
生成的字节码如下
public abstract class Test {
private Test() {
}
// $FF: synthetic method
public Test(DefaultConstructorMarker $constructor_marker) {
this();
}
}
public final class Test1 extends Test {
public Test1() {
super((DefaultConstructorMarker)null);
}
}
public final class Test2 extends Test {
public Test2() {
super((DefaultConstructorMarker)null);
}
}
现在,如您所说,您不会遇到抽象类的问题,让我们深入研究一下:
abstract class TestAbstract
class TestAb1 : TestAbstract()
class TestAb2 : TestAbstract()
生成的字节码如下
public abstract class TestAbstract {
}
public final class TestAb1 extends TestAbstract {
}
public final class TestAb2 extends TestAbstract {
}
现在
如果您已经仔细阅读了代码和字节码,您可能已经观察到:
public abstract class Test {
private Test() {
}
// $FF: synthetic method
public Test(DefaultConstructorMarker $constructor_marker) {
this();
}
}
这是父密封类的字节码,问题是默认构造函数是私有的,并且还有另一个带有DefaultConstructorMarker
作为参数类型的重载构造函数。
现在,由于编译器/ Docker 在编译时生成了一个代理类,该代理类本质上继承了编译的Test
类,它无法找到默认构造函数,因此无法生成类。这就是为什么它抱怨不能继承,即不能继承一个默认构造函数为私有的类。
尝试像下面这样,您将在编辑器中获得错误,就像下面的图片一样:
注意: Hibernate不知道DefaultConstructorMarker
是谁,它是Kotlin JVM类,Kotlin编译器在底层允许通过抽象类创建密封类。但是Hibernate SDK/注解处理器知道它应该获得一个默认的公共构造函数来在生成代理类时调用。
我希望上述的解释和提供的详细信息回答了您的问题。
英文:
I am not an expert in Hibernate, but I know Kotlin, Please go through below
sealed class Test
class Test1 : Test()
class Test2 : Test()
Generated Bytecode would be as follows
public abstract class Test {
private Test() {
}
// $FF: synthetic method
public Test(DefaultConstructorMarker $constructor_marker) {
this();
}
}
public final class Test1 extends Test {
public Test1() {
super((DefaultConstructorMarker)null);
}
}
public final class Test2 extends Test {
public Test2() {
super((DefaultConstructorMarker)null);
}
}
Now as you said you don't face issues with abstract classes let's jump into that,
abstract class TestAbstract
class TestAb1 : TestAbstract()
class TestAb2 : TestAbstract()
Generated bytecode would be as follows
public abstract class TestAbstract {
}
public final class TestAb1 extends TestAbstract {
}
public final class TestAb2 extends TestAbstract {
}
Now
If you have gone through the code and bytecode thoroughly, you might have observed that,
public abstract class Test {
private Test() {
}
// $FF: synthetic method
public Test(DefaultConstructorMarker $constructor_marker) {
this();
}
}
is the byte code for the parent sealed class, what is the deal here, the thing is the default constructor is private and there is another overloaded constructor with DefaultConstructorMarker
as a param type.
Now since the compiler / Docker while does compile i.e. generates a proxy class that would essentially inherit the sealed i.e. the compiled Test
class it is unable to find the default constructor and hence is unable to generate the class. This is why it is complaining that it can not inherit, i.e. can not inherit from a class whose default constructor is private
Try like below you will get the error as in the below picture, in the editor itself.
Note: Hibernate does not know who is DefaultConstructorMarker
, it is a Kotlin JVM class and the Kotlin compiler does the improvisation to allow sealed class creation through abstract class under the hood. But the Hibernate SDK/annotation processor knows that it should get a default public constructor to call when generating a Proxy class.
I hope the above explanation and given details answer the question.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论