5

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]

2 Answers2

3

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. You can try using the @Proxy(lazy = false)` annotation on your entity to disable the creation of proxy instances for that specific entity.

@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.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    This is the correct answer. Hibernate and pretty much any ORM will use a proxy to get / set field values to/from a database. In your case a sealed class limits the interface creation so a proxy cannot be generated. Hacky workaround you can use `data class Attribute which while not eliminating inheritance will keep the contract tidy. – Chris Hinshaw Aug 16 '23 at 04:01
  • @ChrisHinshaw True, you can use a `data class` as an entity. But doing so would bring Kotlin's data classes automatically generated methods like `equals()`, `hashCode()`, and `copy()`. Don't you think these generated methods might *not* behave correctly with JPA's lazy loading or proxying mechanisms? – VonC Aug 16 '23 at 07:40
  • We currently use spring and r2dbc which should function the same. It will likely require the all open 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. https://kotlinlang.org/docs/all-open-plugin.html – Chris Hinshaw Aug 16 '23 at 13:00
  • Actually after thinking about it the https://kotlinlang.org/docs/all-open-plugin.html looks like it works for sealed classes as well. It's worth a try adding it to your plugins to check. – Chris Hinshaw Aug 16 '23 at 13:07
  • @ChrisHinshaw Nice! I have included your comments in the answer for more visibility. Let me know if I have missed anything. – VonC Aug 16 '23 at 13:26
1

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.

abstract class with private default constructor

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.

rahat
  • 1,840
  • 11
  • 24