How Lombok really works (and its performance cost at runtime) — Part 1

Alberto Sanz
adidoescode
Published in
5 min readAug 30, 2024

Come on, all of you.

Raise your hand.

Who is using Lombok here?

Ah, I see. Lots of people.

Wait, keep your hands raised. Now, who knows how Lombok works?

No, no, no. A “you put some annotations and it generates getters and setters” does not answer my question. That answers “How do you use Lombok?”, not “How Lombok works?”

Do you know what are the implications of using Lombok’s @Data? Do you know what it does?

How Lombok works

Me know what Lombok do

Me very smart

Me run delombok

Me see result code

Yes, yes, yes. You very smart. Here you have a badge and an applause from the crowd.

Now that we don’t have that annoying noise anymore, let’s go into details.

Yes, Lombok is a compile-time-only dependency (if configured properly) that runs an annotation processor. This processor checks classes marked with Source Retention Policy annotations to modify the intermediate AST generated by the compiler prior to writing the bytecode into the class file.

🙂

Now hear me well. After this, there is no turning back. You close the browser tab, the story ends, you keep coding the way you do and believe whatever you want to believe. You keep reading, you stay in Wonderland, and I show you how deep the rabbit hole goes. Remember: all I’m offering is the truth. Nothing more.

The Lombok is everywhere. You can feel it when you go to work

How Lombok works (for humans)

OK, let’s dissect previous statement.

Lombok is a compile-time-only dependency…

Everything Lombok does is done at compile time. There’s no need for you to add Lombok to your runtime, and doing it only adds unneeded classes to be loaded by the JVM. Remember that, when started, the JVM needs to load the available classes in the classpath. It’s simple math: the more classes to load, the more time it requires to start. Javascript people takes this way more seriously than Java one and they apply tree shaking techniques to avoid it, but they’re not as common in JVM (unless talking about Android).

… that runs an annotation processor…

An annotation processor is just that, code that is able to process annotations. It’s based in an AbstractProcessor, a feature added in Java 6. It’s a bit uncomfortable to configure, but it’s just a one-time-only action. You can always use Google’s AutoService library for that.

Once you have your annotation processor configured to run, remember: it’s executed at compile time. The input of such processor is your code.

This processor checks classes marked with Source Retention Policy annotations…

Lombok is a compile-time-only dependency, remember? Why would you want to retain its annotations info during the lifetime of your application?

What? You don’t know what Retention Policy means? You know about the lifetime of the application, right? This is the same but for annotations. Annotations are defined in the source code, written into the binary class file and loaded into the JVM so they can be queried via reflection. This is, if you configure the Retention Policy accordingly 😉

As simple as adding an annotation to your annotation!

@Retention(
// three options here, choose wisely
// RetentionPolicy.SOURCE // -> annotation is discarded by the compiler
// RetentionPolicy.CLASS // -> annotation is written into class file, but discarded by the JVM when loading the class
// RetentionPolicy.RUNTIME // -> annotation is written into class file AND loaded by the JVM. Congratulations, you can query it now via reflection
)
public @interface MyFancyAnnotation { }

We’ve already said that Lombok works only at compile-time, so it makes sense that all its annotations are marked with SOURCE retention policy

…to modify the intermediate AST generated by the compiler prior to writing the bytecode into the class file

Source

This whole annotation processing at compile time allows you to do some fancy meta-programming stuff, analyse your code, generate new Java code based on yours (personally, I recommend using Square’s JavaPoet for that, but you can always write raw Strings into the file) and, bear with me, hack you way up the compiler’s AST

In case you didn’t know, the compiler’s AST (AKA Abstract Syntax Tree) is the intermediate, in-memory representation of your code that the compiler generates before doing its thing. We could say that it’s the real machine-readable representation of your code, not that dirty human-made .java file. And since it's stored in memory... And has its own data types & structures... We can modify it 😈

I’m not going to go into details about how to do it, there are plenty of resources online about it. Beware, though, that you’re dealing with code here, and very malicious things can be done. To the level of “unless you decompile the generated .class file, you will not know what's actually going on”. Please, retain yourselves from writing a malicious library that, when added to a project, modifies all your methods (remember, at compile time) so any invocation to any method throws a RuntimeException claiming that you're ugly (please, don't judge code written by a poor me 8 years ago)

Now what?

Now you know the dark magic behind Lombok. And you can build your own Lombok, with blackjack and theme parks! (Please, don’t do it)

We’re dealing with compiler related stuff here. Things that you don’t need to deal with, and you don’t want to deal with. Knowledge is power (France is bacon), and you should be a responsible engineer that knows that being able to do some thing does not mean that you should do that thing.

We have demystified (or mystified a bit, up to you) how Lombok works. We now know that, in the end, using Lombok is the same as writing those methods yourself. Which means that writing

@Data
public class LombokPoint {
private final int x;
private final int y;
}

is actually equivalent to

// actual decompiled code. """Equivalent""" to running delombok
public class LombokPoint {
private final int x;
private final int y;

public LombokPoint(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return this.x;
}

public int getY() {
return this.y;
}

public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof LombokPoint)) {
return false;
} else {
LombokPoint other = (LombokPoint)o;
if (!other.canEqual(this)) {
return false;
} else if (this.getX() != other.getX()) {
return false;
} else {
return this.getY() == other.getY();
}
}
}

protected boolean canEqual(Object other) {
return other instanceof LombokPoint;
}

public int hashCode() {
int PRIME = true;
int result = 1;
result = result * 59 + this.getX();
result = result * 59 + this.getY();
return result;
}

public String toString() {
int var10000 = this.getX();
return "LombokPoint(x=" + var10000 + ", y=" + this.getY() + ")";
}
}

And its performance cost at runtime?

Congratulations! You’ve gone deep down the rabbit hole, you start to approximate a bit to my world

We have now all the context we need for understanding the runtime impact of using Lombok. Reading this part was a bit tough for some of you, so I’ll give you some free time for grabbing a coffee, relaxing and going outside to see the light of the Sun. Stay tuned for the part 2!

The views, thoughts, and opinions expressed in the text belong solely to the author, and do not represent the opinion, strategy or goals of the author’s employer, organization, committee or any other group or individual.

--

--