Christian Beikov
Christian Beikov

Reputation: 16430

Bytecode transforming record class to be mutable

I just saw that EBean does bytecode transformation of record class files in a way that feels odd to me and I seek an answer about whether this is legal from a JVM point of view.

Apparently, it is possible to have a class file, where the class extends java.lang.Record and defines record component attributes (so it's a "record" like javac would create it), but with the following additional "features" which javac would not allow:

To me, this seems illegal and I would have expected a JVM verification error. I would like to know if this is something that is "supported", which I can build upon, or if the lack of verification is a JVM bug. Are records just a Java language feature without JVM support?! I read that final fields of records are "truly final" and can't be changed even through reflection and assumed there must be special JVM support that makes sure records match the Java language semantics...

Upvotes: 10

Views: 1264

Answers (4)

Rob Bygrave
Rob Bygrave

Reputation: 4031

Background

For a background to this question with respect to ORM modelling of concatenated primary keys.

Update 13th Sept

So it's been stated: a bytecode transformer can't remove the final modifier of a record field. So ends the story.


For what its worth I'll add the thoughts that came out of our review.

This issue as we saw it really boiled down to the final modifier and the view record types are a language feature (not a jvm feature) and the issues around what that means.

In that way you can almost paraphrase the posted question as: Are records a language feature or a jvm feature? We could view the first part of the response as - Yes, records are a language feature (hence the requirements on javac with jdk support and semantic requirements of equals/hashCode etc).

All the various questions around breaking record semantics equals/hashCode, accessors, constructors, customisation of those etc - this all reinforced the view that record types are indeed a language feature. We were super happy to get those [false] claims because we could prove via tests that nothing was broken and we could explain the details on why that was.

Q: But it's dodgy removing the final modifier and we broke records right? Well, it went to effectively final / effectively immutable. Another way of looking at that is to play devils advocate and see how to stuff it up - e.g. If we were to create a record instance, partially populate it and hand it off in that partially populated state that would stuff up equals/hashCode. Obviously you don't hand off a partially loaded record / partially initialised instance. Where we ended was more the question around whether record type could go from being a language feature to a jvm feature (would a future jvm assume a final) and thoughts around that.

To be clear, we don't have a failing test or jvm error or anything like that we can point to - there is no case where the ebean bytecode transformer is applied to records and that breaks anything. What we do have is the question around the assumption that record types are a language feature vs jvm feature, and that question of effectively final/effectively immutable vs actual final/immutable [a question of semantics like equals/hashCode vs bytecode & Java memory model "proper construction" etc].

Update 12th Sept

Ultimately I think there are 2 ways to look at this:

  1. Language view: Record types are extremely important, they allow pattern matching are a kind of golden key that will unlock lots of cool language features going forward. The details don't matter and the message is simple - don't anyone **** this up !!

  2. Details view: When we look at the bytecode, semantics, java memory model and we compare to how we would write shallowly immutable types without records we see exactly nothing new. No new bytecode, no different semantics etc. This is typified by ORM @EmbeddedId being an exact match to record type. Similarly the changes ebean needed to make to support record type where exactly none.

Brian read 'mutable' and 'not final' and fired his bazooka and that is fair enough. What the question didn't say was 'effectively immutable', 'effectively final', 'late initialisation' - heck, its even a language feature in kotlin - lateinit.

A bytecode transformation agent that does not even know about record types is lined up for some choice words. What is it actually getting wrong? Well, once you get into the details - nothing.

Q: But the semantics of Record::equals() is new? Not to a bytecode transformer no. The only way to **** this up is for a dev to provide a customised equals/hashCode implementation and for that to not follow the semantics of Record::equals() - but that is on the dev providing the equals/hashCode implementation and not on the bytecode transformer.

Also noting the semantics of Record::equals() match the old and existing @EmbeddedId. This actually isn't new from an ORM perspective.

Q: So ebean supported java records by doing nothing? Well yeah, ebean doesn't require a default constructor and hence we been supporting shallowly immutable types for years. Hence records presented as nothing new. Cool and useful but nothing new.

I'll write up all the details and we will have a review and go from there.

Update 11th Sept - Review session

  • I will look to organise a session for people who are interested in the details around this issue. The questions Brian has posed. What record type bytecode looks like, what the enhancement does and why it does it. What actually occurs when we deal with customisation code in constructor, equals/hashCode, accessors, toString etc (it's actually pretty simple once you understand what its doing).

  • I'll happily take any test using @Embeddable,record,junit5, Java 16. If it fails under enhancement I will buy you a beer! We likely will ask permission to add the test to our test suite (Apache2).

  • Ebean being in the ORM business deals with interesting problems like interception, lazy loading, partial objects, dirty checking etc. Bytecode transformation is a commonly used tool in this space because it can greatly simplify how we handle some tough problems. Record type are nice, interesting and useful but they also don't present anything new to ebean bytecode transformation and in fact have the same semantics and needs of EmbeddedId.

  • Next steps: Have the review session and determine how to proceed.

Update 11th Sept

  • Still no actual evidence of incorrect behaviour, broken semantics, incorrect bytecode
  • Brian has expressed concern that the ebean transformation does not support the semantics of Record::equals(). This concern is misplaced, there is nothing new, different or difficult here as far as the bytecode transformation is concerned. We are now really solidly in the comfort zone of what the ebean transformation does. The chance of a real problem here has significantly dropped. The chance of an issue specific to record type has now gone to almost zero. To explain that, we have no problem with the record supplied equals/hashCode implementations. If people provide customized equals/hashCode implementation then these of course must honor the semantics of record but that aspect is on the author of those implementations - as far as ebean bytecode transformation is concerned it just needs to support a provided implementation (in interception terms) that but this no different to the non-record normal class case. There is nothing new or different here in terms of what the ebean transformation has been doing for 16 years.

Summary

  • Currently there is no evidence of incorrect behaviour, broken semantics, incorrect bytecode
  • Brian is concerned that the bytecode transformation might not handle the cases of customisation of constructor, accessors, equals, or hashCode methods. This is very good news indeed because I'd suggest Brian's concern is misplaced. To explain that, these customisations are not specific to record type and are cases that the bytecode transformation has to deal with in normal classes (normal non-record @Entity and @Embeddable classes). These cases is what ebean has been dealing with for 16 years now and I'd suggest is highly battle tested.
  • To state clearly, an issue in this area around customisation of those methods is extremely unlikely to be specific to record type. Let that sink in.
  • Of course there is a chance we might manage to find a new edge case in this area where the bytecode transformation does work correctly. If you could pick someone to do that, Brian would be your pick. The good news here is that these cases and this area are absolutely in our wheelhouse and I'd be confident of fixing them.

Details

As the author of the bytecode transformation in question I'll just add some details.

TLDR: The intention and my expectation is that the semantics of record (as I understand those semantics) is still 100% honored. At this stage I need to get more clarity on the specific semantics that Brian is unhappy about.

  • The transformed record still looks immutable and acts immutably to code using it (effectively immutable)
  • The semantics of hashcode() and equals() is honored (unchanged)
  • The semantics and result of accessor methods is unchanged
  • The semantics and result of toString() is unchanged
  • The semantics and result of constructor us unchanged

The effective change of this bytecode transformation is that the bytecode is going from 'strictly shallowly immutable at construction' to 'effectively shallowly immutable allowing for some late initialisation'.

The late initialisation that can occur is transparent to any code/bytecode that uses the transformed record - code using the transformed record will experience no difference in behaviour or result and I would suggest no difference in semantics.

Code using this transformed record will still think it is immutable and is not able to mutate it.

For folks familiar with Kotlin lateinit it is a bit similar to that - still effectively immutable but allows for late initialisation of the record fields in question. [With 'late' meaning after construction]

Also noting that the transformation is adding some extra fields, methods and some interception on the accessors all for 'internal use only' and nothing is added to the record publicly in terms of fields or methods - none of this is visible to code using the transformed record. My expectation is that they have not changed the semantics of record but more clarity is required here.

The fields have the final modifier removed to allow late initialisation. This means from a Java memory model perspective we have indeed lost the nice 'final on construction JMM semantic' that we get with final fields in general. I'd be super surprised if this was the specific issue but ideally we get that clarified.


In reviewing the bytecode again and reading the comments above it isn't yet clear to me what the specific semantics of records Brian is especially unhappy about. As I see it the possible options could be:

  • No transformation of records is allowed at all?
  • No extra fields or methods are allowed at all even if they are internal only?
  • We can't go from 'strictly immutable at construction' to 'effectively immutable with late initialisation'? (Noting the associated slight JMM change due to loss of final modifier on those fields)

Again, the semantics and results of all record methods (hashcode, equals, toString, constructor) are unchanged so it would be good to get good clarity on what the specific party foul is and hence what specific semantics are in question.


Edit:

Quick overview example

Before transformation

@Embeddable
public record UserRoleId(Integer userId, String roleId) {}

After transformsation (without synthetic fields and methods, IntelliJ decompile to source form)

@Embeddable
public record UserRoleId(Integer userId, String roleId) implements EntityBean {
  private Integer userId;
  private String roleId;

  public UserRoleId(Integer userId, String roleId) {
    this._ebean_intercept = new InterceptReadWrite(this);
    this._ebean_set_userId(userId);
    this._ebean_set_roleId(roleId);
  }

  public Integer userId() {
    return this._ebean_get_userId();
  }

  public String roleId() {
    return this._ebean_get_roleId();
  }
}

Diff in bytecode:

I've put the before and after in bytecode form in a second answer as otherwise we exceed the character limit here.

Folks reviewing the bytecode, please have a look at that second posted answer.

Edit re customisation of record types

Brian has suggested that "but it only seems to work for records that do not customize the constructor, accessors, equals, or hashCode methods".

This isn't the case. Customisation of all those is expected, allowed, handled + multiple constructors. To explain that a bit more, these cases are not different to the non-record class cases that we have already been dealing with for many years. Ebean is 16 years old, we have been doing bytecode transformation for that time.

Q: Have we made mistakes in the past? Absolutely.

Q: Do we have a mistake in what the transformation is doing with @Embeddable record? At the moment we have no evidence of a mistake. (ok, it's a bug that we have that _ebean_identity field there but I've just fixed that).

Although record type is new'ish (Java 16) the concept of shallow immutability isn't new and bytecode wise records are not too different from immutable types we have been able to code forever in java.

The JPA spec requires default constructor (btw: a restriction soon to be removed it seems) and getters/setters but ebean does not have those restrictions. This means that the customisations that Brian mentions are things that ebean transformation has had to deal with for a very long time - 16 years actually, as these are all the things with expect with non record entity classes. For the mutating entity class case there are other slightly interesting things that we need to deal with (around collections) that we do not for record types.

That is, there isn't any customisation of record types that would be new or different to what the ebean transformer has been dealing with.

Another detail to put in here is that the JVM didn't always enforce final. From vague memory from around Java 8 or so the JVM really did enforce final. This is the sort of little detail that might be concerning/nagging Brian.


Edit:

Absolutely we should not be taking things personally but lets put this in context. I've been in the Java community for 25 years, I'm the organiser of the local JUG, I have an open source project that is 16 years old that is taking a serious reputational hit right here.

Brian Geotz, a literal Java God has said "pretty serious party foul", "shamed out of the community", "ignorance", "poisoning the well" - to someone like me who is a Java fanboy these are literally hammer blows from a God. Being referred to as "the author" doesn't actually soften those blows in case you were wondering, in fact it hurts more because it suggests this isn't a really serious issue. In case it isn't obvious, I'm taking this issue really really really seriously and I'll be pushing to get right to the detail of this and confirm if there is indeed a problem with the bytecode transformation here.

24 hours in and I'm holding up. I might even foolishly be thinking I am starting to get to the heart of the matter. The current TLDR is probably that you should not take on bytecode transformation unless you really know what you are doing. For myself, I've got 16 years of taking bytecode transformation seriously. I'm not ignorant of the size of the challenge and the depth of knowledge you need to get this right. This is not tiddly winks.

At this stage we don't actually have evidence of wrong doing and it's more a suggestion that ebean might be incorrectly handling customisation of record types. This is actually really really good news to me because I've got 16 years of experience to fall back on that suggests that the bytecode transformation does indeed cover all the cases Brian is concerned about (plus other cases thrown in by Kotlin, Scala and Groovy compilers and other cases thrown in by mutable types).

Record types are actually the nice easy case as far as ebean transformation is concerned.

Next steps:

Can we get actual evidence of ebean transformation doing the wrong thing?

Brian might be able to give me a curly example to test out and report back the bytecode. I think this is where we are at.

Upvotes: 14

Brian Goetz
Brian Goetz

Reputation: 95466

Your question posits a false dichotomy. Records are a language feature, with some degree of JVM support (primarily for reflection), but that doesn't mean that the JVM will (or even can) enforce all the requirements on records that the language requires. (Gaps like this are inevitable, as the JVM is a more general computing substrate, and which services other languages besides Java; for example, the JVM permits methods to be overloaded on return type, but the language does not.)

That said, the behavior you describe is a pretty serious party foul, and those who engage in it should be shamed out of the community. (It is also possible they are doing so out of ignorance, in which case they might be educable.) Most likely, these people think they are being "clever" in subverting rules they don't like, but in the process, poisoning the well by promoting behaviors that users may find astonishing.

EDIT

The author of the transformer posted some further context here about what they were trying to accomplish. I'll give them credit for making a good faith effort to conform with the semantics of records, but it undermines the final field semantics, and only appears to work for records that do not customize the constructor, accessors, equals, or hashCode methods. This describes a lot of records, but not all. This is a good cautionary tale; even when trying to preserve the semantics of a class while transforming it, it is easy to make questionable assumptions about what the class does or does not do that can get in the way.

The author waves away the concern about the final field semantics as "not likely to cause a problem." But this is not the bar. The language provides certain semantics for records. This transformation undermines those semantics, and yet still tells the user they are records. Even if they are "minor" and "unlikely", you are breaking the semantics that the Java language promises. "99% compatible" rounds to zero in this case. So I stand by my assertion that this framework is taking inappropriate liberties with the language semantics. They may have been well-intentioned, they may have tried hard to not break things, but break things they did.

Upvotes: 9

Rob Bygrave
Rob Bygrave

Reputation: 4031

Ok, my comments are too big for comments so ...


More notes:

So the enhancement as it is has design aspects orientated to mutating entity beans and so there is bytecode here that isn't strictly required or effectively is a noop for the @Embedded / @EmbeddedId record case:

  • The _ebean_identity field isn't needed at all. Its really a bug that it's there (records -> no inheritance + equals/hashcode implementation).
  • The interception on accessors is effectively a noop in the records case. There are no partials, no lazy loading, always fully populated, no shallow mutation. The interception, preGetter, preSetter etc are all effectively noop. Ideally the enhancement didn't do that for the record case. e.g. The accessors just returned the fields and the constructor just set the fields.
  • The actual interceptor field _ebean_intercept is almost redundant but not quite. We are shallowly immutable but not deap immutable. e.g A record field could be a mutable embeddable or a mutable type like @DbJson and the _ebean_intercept in employed in those 2 cases. Could we lose that for the record case? Maybe, hmmm.
  • There are 2 synthetic constructors. One for InterceptReadWrite and one for InterceptReadOnly. Really in the case with record we could probably always use the InterceptReadOnly and simplify this.

How did we get here?

Well, with ebean we have been dealing with entity and embedded beans that have final fields and non-default constructors for years. These cases go from 'partially' immutable (very common) to 'fully' immutable (no setters and look very similar to records but yes this is rare) and the fully immutable tend to be for the reporting cases (views, aggregations/group by etc). Other ORMs like DataNucleus have also been doing this so for some ORM folks relaxing the final modifier on fields isn't a new thing.

In this sense record in terms of bytecode doesn't look too different to what we have been doing except that they come with hashCode/equals implementation but even this isn't actually any different to what we have been doing for many years - ebean must honor a hashCode/equals implementation if it's been provided etc.

In terms of JMM and removing the final modifier - I'm comfortable that we are not being a bad actor. Perhaps too comfortable but this isn't Valhalla.

Upvotes: 0

Rob Bygrave
Rob Bygrave

Reputation: 4031

Adding a second answer with the bytecode diff as adding the bytecode exceeds the character limit.

Edit2: Before and After in bytecode form.

Before

// class version 60.0 (60)
// RECORD
// access flags 0x10031
public final class org/example/records/UserRoleId extends java/lang/Record {

  // compiled from: UserRoleId.java

  @Ljavax/persistence/Embeddable;()
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
  RECORDCOMPONENT   Ljava/lang/Integer; userId
  RECORDCOMPONENT   Ljava/lang/String; roleId

  // access flags 0x12
  private final Ljava/lang/Integer; userId

  // access flags 0x12
  private final Ljava/lang/String; roleId

  // access flags 0x1
  public <init>(Ljava/lang/Integer;Ljava/lang/String;)V
    // parameter  userId
    // parameter  roleId
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Record.<init> ()V
    ALOAD 0
    ALOAD 1
    PUTFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    ALOAD 0
    ALOAD 2
    PUTFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    RETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    LOCALVARIABLE userId Ljava/lang/Integer; L0 L1 1
    LOCALVARIABLE roleId Ljava/lang/String; L0 L1 2
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x11
  public final toString()Ljava/lang/String;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEDYNAMIC toString(Lorg/example/records/UserRoleId;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final hashCode()I
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEDYNAMIC hashCode(Lorg/example/records/UserRoleId;)I [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    IRETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final equals(Ljava/lang/Object;)Z
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    INVOKEDYNAMIC equals(Lorg/example/records/UserRoleId;Ljava/lang/Object;)Z [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    IRETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    LOCALVARIABLE o Ljava/lang/Object; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public userId()Ljava/lang/Integer;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public roleId()Ljava/lang/String;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

After

// class version 60.0 (60)
// RECORD
// access flags 0x10031
public final class org/example/records/UserRoleId extends java/lang/Record implements io/ebean/bean/EntityBean {

  // compiled from: UserRoleId.java

  @Ljavax/persistence/Embeddable;()
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
  RECORDCOMPONENT   Ljava/lang/Integer; userId
  RECORDCOMPONENT   Ljava/lang/String; roleId

  // access flags 0x2
  private Ljava/lang/Integer; userId

  // access flags 0x2
  private Ljava/lang/String; roleId

  // access flags 0x1009
  public static synthetic [Ljava/lang/String; _ebean_props

  // access flags 0x1004
  protected synthetic Lio/ebean/bean/EntityBeanIntercept; _ebean_intercept

  // access flags 0x1084
  protected transient synthetic Ljava/lang/Object; _ebean_identity

  // access flags 0x1
  public <init>(Ljava/lang/Integer;Ljava/lang/String;)V
    // parameter  userId
    // parameter  roleId
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Record.<init> ()V
    ALOAD 0
    NEW io/ebean/bean/InterceptReadWrite
    DUP
    ALOAD 0
    INVOKESPECIAL io/ebean/bean/InterceptReadWrite.<init> (Ljava/lang/Object;)V
    PUTFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ALOAD 0
    ALOAD 1
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_set_userId (Ljava/lang/Integer;)V
    ALOAD 0
    ALOAD 2
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_set_roleId (Ljava/lang/String;)V
    RETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    LOCALVARIABLE userId Ljava/lang/Integer; L0 L1 1
    LOCALVARIABLE roleId Ljava/lang/String; L0 L1 2
    MAXSTACK = 4
    MAXLOCALS = 3

  // access flags 0x11
  public final toString()Ljava/lang/String;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEDYNAMIC toString(Lorg/example/records/UserRoleId;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final hashCode()I
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEDYNAMIC hashCode(Lorg/example/records/UserRoleId;)I [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    IRETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final equals(Ljava/lang/Object;)Z
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    INVOKEDYNAMIC equals(Lorg/example/records/UserRoleId;Ljava/lang/Object;)Z [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
      // arguments:
      org.example.records.UserRoleId.class, 
      "userId;roleId", 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.userId(Ljava/lang/Integer;), 
      // handle kind 0x1 : GETFIELD
      org/example/records/UserRoleId.roleId(Ljava/lang/String;)
    ]
    IRETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    LOCALVARIABLE o Ljava/lang/Object; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public userId()Ljava/lang/Integer;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_userId ()Ljava/lang/Integer;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public roleId()Ljava/lang/String;
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_roleId ()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 1 L0
    ICONST_2
    ANEWARRAY java/lang/String
    DUP
    ICONST_0
    LDC "userId"
    AASTORE
    DUP
    ICONST_1
    LDC "roleId"
    AASTORE
    PUTSTATIC org/example/records/UserRoleId._ebean_props : [Ljava/lang/String;
   L1
    LINENUMBER 1 L1
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0

  // access flags 0x1001
  public synthetic <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Record.<init> ()V
    ALOAD 0
    NEW io/ebean/bean/InterceptReadWrite
    DUP
    ALOAD 0
    INVOKESPECIAL io/ebean/bean/InterceptReadWrite.<init> (Ljava/lang/Object;)V
    PUTFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
   L1
    LINENUMBER 2 L1
    RETURN
   L2
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L2 0
    MAXSTACK = 4
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic _ebean_getPropertyNames()[Ljava/lang/String;
   L0
    LINENUMBER 13 L0
    GETSTATIC org/example/records/UserRoleId._ebean_props : [Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic _ebean_getPropertyName(I)Ljava/lang/String;
   L0
    LINENUMBER 16 L0
    GETSTATIC org/example/records/UserRoleId._ebean_props : [Ljava/lang/String;
    ILOAD 1
    AALOAD
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    LOCALVARIABLE pos I L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1001
  public synthetic _ebean_getIntercept()Lio/ebean/bean/EntityBeanIntercept;
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic _ebean_intercept()Lio/ebean/bean/EntityBeanIntercept;
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    IFNONNULL L1
   L2
    LINENUMBER 2 L2
    ALOAD 0
    NEW io/ebean/bean/InterceptReadWrite
    DUP
    ALOAD 0
    INVOKESPECIAL io/ebean/bean/InterceptReadWrite.<init> (Ljava/lang/Object;)V
    PUTFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
   L1
    LINENUMBER 3 L1
   FRAME SAME
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ARETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    MAXSTACK = 4
    MAXLOCALS = 1

  // access flags 0x1004
  protected synthetic _ebean_get_userId()Ljava/lang/Integer;
   L0
    LINENUMBER 6 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_0
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.preGetter (I)V (itf)
   L1
    LINENUMBER 7 L1
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    ARETURN
   L2
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1004
  protected synthetic _ebean_set_userId(Ljava/lang/Integer;)V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_1
    ICONST_0
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_userId ()Ljava/lang/Integer;
    ALOAD 1
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.preSetter (ZILjava/lang/Object;Ljava/lang/Object;)V (itf)
   L1
    LINENUMBER 2 L1
    ALOAD 0
    ALOAD 1
    PUTFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
   L2
    LINENUMBER 4 L2
    RETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    LOCALVARIABLE newValue Ljava/lang/Integer; L0 L3 1
    MAXSTACK = 5
    MAXLOCALS = 2

  // access flags 0x1004
  protected synthetic _ebean_getni_userId()Ljava/lang/Integer;
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1004
  protected synthetic _ebean_setni_userId(Ljava/lang/Integer;)V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
   L1
    LINENUMBER 2 L1
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_0
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.setLoadedProperty (I)V (itf)
   L2
    LINENUMBER 1 L2
    RETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    LOCALVARIABLE _newValue Ljava/lang/Integer; L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1004
  protected synthetic _ebean_get_roleId()Ljava/lang/String;
   L0
    LINENUMBER 6 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_1
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.preGetter (I)V (itf)
   L1
    LINENUMBER 7 L1
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    ARETURN
   L2
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1004
  protected synthetic _ebean_set_roleId(Ljava/lang/String;)V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_1
    ICONST_1
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_roleId ()Ljava/lang/String;
    ALOAD 1
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.preSetter (ZILjava/lang/Object;Ljava/lang/Object;)V (itf)
   L1
    LINENUMBER 2 L1
    ALOAD 0
    ALOAD 1
    PUTFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
   L2
    LINENUMBER 4 L2
    RETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    LOCALVARIABLE newValue Ljava/lang/String; L0 L3 1
    MAXSTACK = 5
    MAXLOCALS = 2

  // access flags 0x1004
  protected synthetic _ebean_getni_roleId()Ljava/lang/String;
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1004
  protected synthetic _ebean_setni_roleId(Ljava/lang/String;)V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
   L1
    LINENUMBER 2 L1
    ALOAD 0
    GETFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
    ICONST_1
    INVOKEINTERFACE io/ebean/bean/EntityBeanIntercept.setLoadedProperty (I)V (itf)
   L2
    LINENUMBER 1 L2
    RETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    LOCALVARIABLE _newValue Ljava/lang/String; L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1001
  public synthetic _ebean_getField(I)Ljava/lang/Object;
   L0
    LINENUMBER 1 L0
    ILOAD 1
    TABLESWITCH
      0: L1
      1: L2
      default: L3
   L1
    LINENUMBER 1 L1
   FRAME SAME
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    ARETURN
   L2
    LINENUMBER 1 L2
   FRAME SAME
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    ARETURN
   L3
    LINENUMBER 1 L3
   FRAME SAME
    NEW java/lang/RuntimeException
    DUP
    NEW java/lang/StringBuilder
    DUP
    LDC "Invalid index "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESPECIAL java/lang/RuntimeException.<init> (Ljava/lang/String;)V
    ATHROW
   L4
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L4 0
    LOCALVARIABLE index I L0 L4 1
    MAXSTACK = 5
    MAXLOCALS = 2

  // access flags 0x1001
  public synthetic _ebean_getFieldIntercept(I)Ljava/lang/Object;
   L0
    LINENUMBER 1 L0
    ILOAD 1
    TABLESWITCH
      0: L1
      1: L2
      default: L3
   L1
    LINENUMBER 1 L1
   FRAME SAME
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_userId ()Ljava/lang/Integer;
    ARETURN
   L2
    LINENUMBER 1 L2
   FRAME SAME
    ALOAD 0
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_get_roleId ()Ljava/lang/String;
    ARETURN
   L3
    LINENUMBER 1 L3
   FRAME SAME
    NEW java/lang/RuntimeException
    DUP
    NEW java/lang/StringBuilder
    DUP
    LDC "Invalid index "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESPECIAL java/lang/RuntimeException.<init> (Ljava/lang/String;)V
    ATHROW
   L4
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L4 0
    LOCALVARIABLE index I L0 L4 1
    MAXSTACK = 5
    MAXLOCALS = 2

  // access flags 0x1001
  public synthetic _ebean_setField(ILjava/lang/Object;)V
   L0
    LINENUMBER 1 L0
    LINENUMBER 1 L0
    ILOAD 1
    TABLESWITCH
      0: L1
      1: L2
      default: L3
   L1
    LINENUMBER 1 L1
   FRAME SAME
    ALOAD 0
    ALOAD 2
    CHECKCAST java/lang/Integer
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_setni_userId (Ljava/lang/Integer;)V
   L4
    LINENUMBER 1 L4
    RETURN
   L2
    LINENUMBER 1 L2
   FRAME SAME
    ALOAD 0
    ALOAD 2
    CHECKCAST java/lang/String
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_setni_roleId (Ljava/lang/String;)V
   L5
    LINENUMBER 1 L5
    RETURN
   L3
    LINENUMBER 1 L3
   FRAME SAME
    NEW java/lang/RuntimeException
    DUP
    NEW java/lang/StringBuilder
    DUP
    LDC "Invalid index "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESPECIAL java/lang/RuntimeException.<init> (Ljava/lang/String;)V
    ATHROW
   L6
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L6 0
    LOCALVARIABLE index I L0 L6 1
    LOCALVARIABLE o Ljava/lang/Object; L0 L6 2
    LOCALVARIABLE arg Ljava/lang/Object; L0 L6 3
    LOCALVARIABLE p Lorg/example/records/UserRoleId; L0 L6 4
    MAXSTACK = 5
    MAXLOCALS = 5

  // access flags 0x1001
  public synthetic _ebean_setFieldIntercept(ILjava/lang/Object;)V
   L0
    LINENUMBER 1 L0
    LINENUMBER 1 L0
    ILOAD 1
    TABLESWITCH
      0: L1
      1: L2
      default: L3
   L1
    LINENUMBER 1 L1
   FRAME SAME
    ALOAD 0
    ALOAD 2
    CHECKCAST java/lang/Integer
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_set_userId (Ljava/lang/Integer;)V
   L4
    LINENUMBER 1 L4
    RETURN
   L2
    LINENUMBER 1 L2
   FRAME SAME
    ALOAD 0
    ALOAD 2
    CHECKCAST java/lang/String
    INVOKEVIRTUAL org/example/records/UserRoleId._ebean_set_roleId (Ljava/lang/String;)V
   L5
    LINENUMBER 1 L5
    RETURN
   L3
    LINENUMBER 1 L3
   FRAME SAME
    NEW java/lang/RuntimeException
    DUP
    NEW java/lang/StringBuilder
    DUP
    LDC "Invalid index "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESPECIAL java/lang/RuntimeException.<init> (Ljava/lang/String;)V
    ATHROW
   L6
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L6 0
    LOCALVARIABLE index I L0 L6 1
    LOCALVARIABLE o Ljava/lang/Object; L0 L6 2
    LOCALVARIABLE arg Ljava/lang/Object; L0 L6 3
    LOCALVARIABLE p Lorg/example/records/UserRoleId; L0 L6 4
    MAXSTACK = 5
    MAXLOCALS = 5

  // access flags 0x1001
  public synthetic _ebean_setEmbeddedLoaded()V
   L0
    LINENUMBER 1 L0
    RETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic _ebean_isEmbeddedNewOrDirty()Z
   L0
    LINENUMBER 1 L0
    ICONST_0
    IRETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic _ebean_newInstance()Ljava/lang/Object;
   L0
    LINENUMBER 10 L0
    NEW org/example/records/UserRoleId
    DUP
    INVOKESPECIAL org/example/records/UserRoleId.<init> ()V
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x4
  protected synthetic <init>(Lio/ebean/bean/EntityBean;)V
   L0
    LINENUMBER 2 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Record.<init> ()V
   L1
    LINENUMBER 3 L1
    ALOAD 0
    NEW io/ebean/bean/InterceptReadOnly
    DUP
    ALOAD 0
    INVOKESPECIAL io/ebean/bean/InterceptReadOnly.<init> (Ljava/lang/Object;)V
    PUTFIELD org/example/records/UserRoleId._ebean_intercept : Lio/ebean/bean/EntityBeanIntercept;
   L2
    LINENUMBER 25 L2
    RETURN
   L3
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L3 0
    LOCALVARIABLE ignore Lio/ebean/bean/EntityBean; L0 L3 1
    MAXSTACK = 4
    MAXLOCALS = 2

  // access flags 0x1
  public synthetic _ebean_newInstanceReadOnly()Ljava/lang/Object;
   L0
    LINENUMBER 4 L0
    NEW org/example/records/UserRoleId
    DUP
    ACONST_NULL
    INVOKESPECIAL org/example/records/UserRoleId.<init> (Lio/ebean/bean/EntityBean;)V
    ARETURN
   L1
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic toString(Lio/ebean/bean/ToStringBuilder;)V
   L0
    LINENUMBER 2 L0
    ALOAD 1
    ALOAD 0
    INVOKEVIRTUAL io/ebean/bean/ToStringBuilder.start (Ljava/lang/Object;)V
   L1
    LINENUMBER 3 L1
    ALOAD 1
    LDC "userId"
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.userId : Ljava/lang/Integer;
    INVOKEVIRTUAL io/ebean/bean/ToStringBuilder.add (Ljava/lang/String;Ljava/lang/Object;)V
   L2
    LINENUMBER 3 L2
    ALOAD 1
    LDC "roleId"
    ALOAD 0
    GETFIELD org/example/records/UserRoleId.roleId : Ljava/lang/String;
    INVOKEVIRTUAL io/ebean/bean/ToStringBuilder.add (Ljava/lang/String;Ljava/lang/Object;)V
   L3
    LINENUMBER 4 L3
    ALOAD 1
    INVOKEVIRTUAL io/ebean/bean/ToStringBuilder.end ()V
   L4
    LINENUMBER 5 L4
    RETURN
   L5
    LOCALVARIABLE this Lorg/example/records/UserRoleId; L0 L5 0
    LOCALVARIABLE sb Lio/ebean/bean/ToStringBuilder; L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2
}

Notes:

  • The synthetic mutation methods exist (so potential for abuse). The "promise" is that Ebean will not mutate after any public methods are called - hashcode, equals, toString, accessors. The hashcode value must not change, equals must not change. Ebean must honor this promise.
  • This somewhat boils down to not mutating after an accessor is called as hashcode, equals, toString use accessors via method handles.
  • The current bytecode enhancement does not currently detect and treat records differently. e.g. the _ebean_identity field isn't needed or used for records at all.
  • Ebean has 2 interception modes - read-only and read-write. As records are always read-only its possible that record specific enhancement could be simplified to reflect that (hmmm).

Upvotes: 0

Related Questions