Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hibernate bytecode enhancement applied by quarkus generates invalid bytecode for embeddedId value objects #20186

Closed
thomasdarimont opened this issue Sep 15, 2021 · 11 comments · Fixed by #20287
Assignees
Labels
area/hibernate-orm Hibernate ORM area/persistence OBSOLETE, DO NOT USE kind/bug Something isn't working
Milestone

Comments

@thomasdarimont
Copy link
Contributor

thomasdarimont commented Sep 15, 2021

Describe the bug

Given the following JPA Entity class with an EmbeddedId class modeled as a value Object, the Hibernate bytecode enhancement that is triggered by Quarkus generates invalid bytecode (shown below). This leads to java.lang.IllegalAccessError: Update to non-static final field com.example.Drink$DrinkId.id attempted from a different method ($$_hibernate_write_id) than the initializer method <init> errors.

Example class

package com.example;

import org.hibernate.annotations.Immutable;

import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;

@Entity
public class Drink {

    @EmbeddedId
    private DrinkId id;

    private String name;

    public Drink() {
        this.id = DrinkId.of(UUID.randomUUID().toString());
    }

    public DrinkId getId() {
        return id;
    }

    public void setId(DrinkId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Immutable
    @Embeddable
    public static class DrinkId implements Serializable {  // invalid bytecode is generated for this class

        private final String id;

        public DrinkId() {
            this.id = null;
        }

        private DrinkId(String id) {
            this.id = id;
        }

        public static DrinkId of(String string) {
            return new DrinkId(string);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof DrinkId)) return false;
            DrinkId drinkId = (DrinkId) o;
            return Objects.equals(id, drinkId.id);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }
}

Stacktrace:

java.lang.IllegalAccessError: Update to non-static final field com.example.Drink$DrinkId.id attempted from a different method ($$_hibernate_write_id) than the initializer method <init> 
	at com.example.Drink$DrinkId.$$_hibernate_write_id(Drink.java)
	at com.example.Drink$DrinkId.<init>(Drink.java:50)
	at com.example.Drink$DrinkId.of(Drink.java:54)
	at com.example.Drink.<init>(Drink.java:20)
	at com.example.ExampleResource.hello(ExampleResource.java:25)
	at com.example.ExampleResource_Subclass.hello$$superforward1(ExampleResource_Subclass.zig:157)
	at com.example.ExampleResource_Subclass$$function$$1.apply(ExampleResource_Subclass$$function$$1.zig:24)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:54)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.proceed(InvocationInterceptor.java:62)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.monitor(InvocationInterceptor.java:49)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor_Bean.intercept(InvocationInterceptor_Bean.zig:521)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:50)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:132)
....
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:833)
Resulted in: org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalAccessError: Update to non-static final field com.example.Drink$DrinkId.id attempted from a different method ($$_hibernate_write_id) than the initializer method <init> 
	at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:106)
	at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:372)
	at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:218)
	at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:519)
	... 15 more

The decompiled generated bytecode for the class DrinkId looks like this:

 @Immutable
  @Embeddable
  public static final class DrinkIdentifier implements Identifier, Serializable, ManagedComposite, PersistentAttributeInterceptable, CompositeTracker {

    private final String id;
    
    @Transient
    private transient PersistentAttributeInterceptor $$_hibernate_attributeInterceptor;
    
    @Transient
    private transient CompositeOwnerTracker $$_hibernate_compositeOwners;
    
    private DrinkIdentifier(String id) {
      $$_hibernate_write_id(id); // This triggers the IllegalAccessError 
    }
...

    public void $$_hibernate_write_id(String param1String) {
      if (this.$$_hibernate_compositeOwners != null)
        this.$$_hibernate_compositeOwners.callOwner(""); 
      if ($$_hibernate_getInterceptor() != null) {
        this.id = (String)$$_hibernate_getInterceptor().writeObject(this, "id", this.id, param1String);
        return;
      } 
      this.id = param1String; // This triggers the IllegalAccessError
    }

Expected behavior

The hibernate bytecode enhancement applied by quarkus should not emit unnecessary / invalid change tracking bytecode.

Actual behavior

The hibernate bytecode enhancement applied by quarkus emits invalid bytecode which leads to errors at runtime.

How to Reproduce?

  1. Download and build the example https://github.com/thomasdarimont/quarkus-bugs-invalid-hibernate-bytecode
  2. Open http://localhost:8080/hello
  3. Observe the stacktrace

Output of uname -a or ver

Linux neumann 5.13.14-200.fc34.x86_64 #1 SMP Fri Sep 3 15:33:01 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

openjdk version "17" 2021-09-14 OpenJDK Runtime Environment (build 17+35-2724) OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

2.2.2.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)

Additional information

I'm not 100% sure if this is a quarkus issue or a plain hibernate issue.

The original example comes from a Spring Boot application (https://github.com/odrotbohm/spring-restbucks/blob/main/server/src/main/java/org/springsource/restbucks/drinks/Drink.java#L53) which uses Hibernate 5.5.6. In the spring app the bytecode of the DrinkIdentifier class is not enhanced.

The quarkus version 2.2.2.Final seems to use Hibernate 5.5.7. Perhaps this is an hibernate issue that is
triggered by quarkus due to the explicit bytecode enhancement...

@thomasdarimont thomasdarimont added the kind/bug Something isn't working label Sep 15, 2021
@quarkus-bot quarkus-bot bot added area/hibernate-orm Hibernate ORM area/persistence OBSOLETE, DO NOT USE labels Sep 15, 2021
@quarkus-bot
Copy link

quarkus-bot bot commented Sep 15, 2021

/cc @Sanne, @gsmet, @yrodiere

@geoand
Copy link
Contributor

geoand commented Sep 16, 2021

Thanks for reporting!

@Sanne this looks like an issue in Hibernate itself, not Quarkus. Correct?

@yrodiere
Copy link
Member

@geoand Quarkus does its own bytecode enhancement on Hibernate ORM entities too, so it might be either, or both. We need to investigate to be sure. I'd be inclined to blame Quarkus in this case, though.

@Sanne
Copy link
Member

Sanne commented Sep 16, 2021

I'm not sure which one it would be; might be best to test in ORM upstream first.

@yrodiere
Copy link
Member

yrodiere commented Sep 16, 2021

Actually I didn't look at the reproducer close enough; the problem is caused by the property being final, not by the fact it's an identifier like I thought.

I'm not sure final properties have ever been supported in embeddables. I don't think Hibernate ORM supports final, non-transient properties? When Hibernate ORM will eventually have to instantiate the DrinkId, it will do so though the default constructor, and it will call setters afterwards. So a final property just cannot work.

Or am I just missing something?

@thomasdarimont
Copy link
Contributor Author

thomasdarimont commented Sep 16, 2021

@yrodiere et al. thanks for looking into this :)

The same thing (with private final being added via lombok) works fine (without bytecode enhancement) with Spring Data JPA backend by hibernate.

...
	@Value(staticConstructor = "of")
	public static class DrinkIdentifier implements Identifier {
		String id;
	}
...

In the Spring-Restbucks example the following (decompiled) Java bytecode is generated for the DrinkIdentifier class:

@Embeddable
  public static final class DrinkIdentifier implements Identifier, Serializable {
    private final String id;
    
    private DrinkIdentifier(String id) {
      this.id = id;
    }
    
    public static DrinkIdentifier of(String id) {
      return new DrinkIdentifier(id);
    }
    
    public boolean equals(Object o) {
      if (o == this)
        return true; 
      if (!(o instanceof DrinkIdentifier))
        return false; 
      DrinkIdentifier other = (DrinkIdentifier)o;
      Object this$id = getId(), other$id = other.getId();
      return !((this$id == null) ? (other$id != null) : !this$id.equals(other$id));
    }
    
    public int hashCode() {
      int PRIME = 59;
      result = 1;
      Object $id = getId();
      return result * 59 + (($id == null) ? 43 : $id.hashCode());
    }
    
    public String toString() {
      return "Drink.DrinkIdentifier(id=" + getId() + ")";
    }
    
    public String getId() {
      return this.id;
    }
    
    public DrinkIdentifier() {
      this;
    }
  }

this works well.

@Sanne
Copy link
Member

Sanne commented Sep 16, 2021

yes it should be OK to have immutable identifiers - and in this case the embeddable component is the identifier.

@yrodiere yrodiere self-assigned this Sep 17, 2021
@yrodiere
Copy link
Member

Confirmed as a bug in Hibernate ORM.

Upstream ticket: https://hibernate.atlassian.net/browse/HHH-14828

I'll see how that can be fixed.

@yrodiere
Copy link
Member

hibernate/hibernate-orm#4231 should fix the problem. Once it's merged and released, we can upgrade Hibernate ORM in Quarkus and get the fix.

@Sanne
Copy link
Member

Sanne commented Sep 20, 2021

Merged the fix, it will be in the next version:

Thanks for the fix @yrodiere , and thanks @thomasdarimont for pointing it out!

@thomasdarimont
Copy link
Contributor Author

Thank you guys for the quick fix :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/hibernate-orm Hibernate ORM area/persistence OBSOLETE, DO NOT USE kind/bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants