Utilities - lombok
This note is about lombok annotations and how they reduce boilerplate code and help generate and keep in sync core methods
references:
intro
project lombok or just lombok
is a library that lifts the weight off the most common boilerplate code found
in java apps.
it is commonly used to avoid writing standard getters / setters, equals + hashcode, adding in loggers or taking care of various constructors and builders among many other goodies.
lombok
is praised and detracted here and there, if you are interested about those types of disputes here is
a reddit thread.
it remains to see what will happen in the future with the JDK APIs lombok use.
if you are sincerely interested in how lombok
does what it does, and criticise or praise them for their work check them out over
their GitHub repo!
for my part the benefits of using it greatly outweigh the possible penalties that may or may not occur in the future. If for any reason lombok stops working with a future Java release, I’ll delombok the app and that’s it.
java 21 and lombok as of this writing
there was a small change in the JDK and lombok will not work on Java 21 for the moment.
note:
at the time of this writing JDK 21 is in ea and is expected to be ga by 2023/09/19
jdk 21 info
specifically:
JDK 21 is changing JCTree
to JCFieldAccess
and lombok refers to the former type
JDK 20 com.sun.tools.javac.tree.JCTree
public static class JCImport extends JCTree implements ImportTree {
public JCTree qualid;
public com.sun.tools.javac.code.Scope importScope;
protected JCImport(JCTree qualid, boolean importStatic) {
this.qualid = qualid;
this.staticImport = importStatic;
}
}
JDK 21 com.sun.tools.javac.tree.JCTree
public static class JCImport extends JCTree implements ImportTree {
public JCFieldAccess qualid;
public com.sun.tools.javac.code.Scope importScope;
protected JCImport(JCFieldAccess qualid, boolean importStatic) {
this.qualid = qualid;
this.staticImport = importStatic;
}
}
there is a fix in the making already here if you are interested
lombok usage in BMC_Anvil
there are 2 main usages across the app:
- getters and setters
- equals and hashcode
the BaseEntity.java
:
@MappedSuperclass
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public abstract class BaseEntity {
@Id
@EqualsAndHashCode.Include
private UUID id;
@ManyToOne(fetch = LAZY)
private UserEntity createdBy;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
the above is the base entity
for the application’s data access and will show a few usages of lombok
here and on inheritors.
as this is a lombok
note we are going to pass on JPA annotations and concentrate only on lombok
ones.
get / set
the 2 annotations @Getter
and @Setter
create for each field a public TYPE getFieldName()
and public void setFieldName(final TYPE fieldName)
.
this is very practical, saving us a lot of boilerplate and when prototyping not having to care for adding or deleting get/set is a bliss.
equals and hashcode
.equals()
will define what makes 2 objects to be “equal” to one another by implementing what equivalent
means for a given data type.
we use .equals()
to know if an object “"”IS the same as””” another object regarding certain parameters.
with our example entity
above when can we say we retrieve the same employee record from the DB?
we could say we have the same employee record based on a person’s first and/or last name… but…
we could say we have the same employee record based on a person’s passport… but…
we could say we have the same employee record based on a person’s SSN… AHA!
what makes a difference among the 3 possible equality choices is the uniqueness and immutability of the data chosen?
a person can change names or have the same as another, and the passport number although unique, will change once renewed.
The SSN on the other hand, will remain unique and immutable for life. So if we retrieve a person’s record and want to know if it is equal to
one we have in memory, we apply the .equals()
which will compare the SSN of both objects.
.hashcode()
will transform our object into an int value. There are some rules to follow thou as
specified in the docs that tie it tightly
to .equals()
.
we need these 2 methods overridden in our entities with our custom definition of equality for each as they are going to be used in hash
based collections such as HashSet
.
let’s see what lombok
can do for us to avoid the equals
and hashcode
boilerplate.
the annotation that generates both methods is @EqualsAndHashCode(onlyExplicitlyIncluded = true)
. The addition
of onlyExplicitlyIncluded = true
means to only consider the fields we explicitly include for generating both methods. That is achieved by
annotating the desired fields with @EqualsAndHashCode.Include
.
in our entity, the only method marked as included is the private UUID id
. Given that UUIDs are unique, and once we save a record they will
never change, we can safely say that if 2 records have the same id
they are the same record…
showcase / teaching note:
can you see down the road why UUIDs were chosen over auto-incrementing values?
among other things, it makes comparing 2 records trivial as we can target a single field.
what will those annotations generate?
regarding equals():
public abstract class BaseEntity {
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof final BaseEntity other)) {
return false;
} else {
if (!other.canEqual(this)) {
return false;
} else {
Object this$id = this.getId();
Object other$id = other.getId();
if (this$id == null) {
return other$id == null;
} else return this$id.equals(other$id);
}
}
}
}
so after doing some de rigueur comparisons, lombok
generates what we expected, we compare 2 objects based on their ids:
if(!this$id.equals(other$id)){
return false;
}
regarding hashCode():
public abstract class BaseEntity {
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.getId();
result = result * 59 + ($id == null ? 43 : $id.hashCode());
return result;
}
}
disregarding the technique used, we see lombok
is also using the id
to generate a hash of the object.
the advantage of using lombok
here, is that if we change what is included in the equality, both methods will be updated without requiring
manual intervention.
inheritance and lombok
that base entity is used as the common set of properties for all other entities, how do we propagate equals / hashcode to inheritors?
let check an example:
@Entity
@Table(name = "comment")
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
public class CommentEntity extends BaseEntity {
@Column(columnDefinition = "text")
private String comment;
@ManyToOne
private CardEntity card;
}
in this class we see again the getter and setter plus a slightly modified call
to @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
, it has the extra parameter callSuper = true
. That parameter
means to go to the super class to look for what defines equals/hashcode there.
when we check the resulting class this is what we will see:
public abstract class BaseEntity {
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof final CommentEntity other)) {
return false;
} else {
if (!other.canEqual(this)) {
return false;
} else {
return super.equals(o);
}
}
}
public int hashCode() {
int result = super.hashCode();
return result;
}
}
as you can see the generated code calls on BaseEntity
methods, again keeping everything in sync without manual intervention.
but what if we want to keep the BaseEntity
methods that use the id
as equality parameter AND add another one, say the comment
field?
we just annotate the field with @EqualsAndHashCode.Include
and lombok
will use the parent’s method plus the new field to generate
equals / hashCode.
it does not make sense in our case and this tip is for demonstration purposes only, so let’s check what would lombok have generated if
we had included the comment
field.
public abstract class BaseEntity {
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof final CommentEntity other)) {
return false;
} else {
if (!other.canEqual(this)) {
return false;
} else if (!super.equals(o)) {
return false;
} else {
Object this$comment = this.getComment();
Object other$comment = other.getComment();
if (this$comment == null) {
return other$comment == null;
} else return this$comment.equals(other$comment);
}
}
}
public int hashCode() {
int PRIME = true;
int result = super.hashCode();
Object $comment = this.getComment();
result = result * 59 + ($comment == null ? 43 : $comment.hashCode());
return result;
}
}
as you can see, both methods include the comment field in both methods and the parent’s ones too.