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]

That looks like a conflict between Kotlin's sealed classes and Hibernate's proxy creation.
When you run this code with Hibernate in IntelliJ, Hibernate is trying to create a proxy subclass of
Attribute. SinceAttributeis a sealed class, Kotlin denies this subclassing, hence theIncompatibleClassChangeErrorerror.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: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. You can try using the@Proxy(lazy = false)` annotation on your entity to disable the creation of proxy instances for that specific entity.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
finalby default. In the Kotlin world, a common workaround for the Hibernate proxy issue is to use thekotlin-allopenGradle plugin. That plugin makes specified classes (e.g., those annotated with@Entityor@MappedSuperclass) non-final during compilation, allowing Hibernate to subclass them for proxy creation.So, if the OP has configured their Gradle build with
kotlin-allopento treat classes annotated with@Entityor@MappedSuperclassas open, then those classes should be able to be proxied by Hibernate.However, the problem here is not with a
finalclass but with asealedclass. 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(), andcopy(), generated methods which might not behave correctly with JPA's lazy loading or proxying mechanisms.But Chris confirmed:
(possible code:
hibernate/hibernate-ormhibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java)However, as noted in the comments by the OP Botond Németh, it is important to note that using data classes as JPA entities is generally not recommended (See
tut-spring-boot-kotlin/ Persistence with JPA), because:Data classes automatically generate
equals()andhashCode()methods. Although Chris mentioned that Hibernate has filters to skip these methods during proxying, the auto-generated methods can still conflict with JPA's mechanisms for proxying and lazy loading.Data classes also generate a
copy()function, and it is unclear how well this will play with JPA's lifecycle and state management. It might introduce unexpected behavior, especially when dealing with persisted objects.So, even though Hibernate may have specific filters to deal with some of these methods, using data classes as JPA entities could still introduce subtle issues and is generally not aligned with best practices.