Jilt version 1.5 released!
Two months ago, I wrote about how a resurgence of interest in Jilt, an open-source Java library that I created eight years ago for automatically generating Builder classes, has resulted in a release of it with new functionality for the first time since 2018.
Well, the interest in the library has remained high,
and a few issues have been raised in the project’s repository since January.
I’ve managed to resolve several of them,
and published a new release, 1.5
,
with those changes.
Let’s quickly go through the highlights of this latest release.
Generate toBuilder()
methods
There is a common pattern that is often used with Lombok,
where the target class is generated with a method called toBuilder()
that returns an instance of the Builder initialized with the values of properties taken from the instance that toBuilder()
was called on.
This is useful to create a copy of the target instance with only a few properties changed,
without having to make the class itself mutable with setters, for example:
Person person = PersonBuilder.person()
.name("Jack")
// other properties...
.build();
Person copiedPerson = person.toBuilder()
.name("John") // example of changing a single property when copying the instance
.build();
In the above example, copiedPerson
will be almost identical to person
,
with the only exception being the name
property, which will be set to "John"
.
Unfortunately, annotation processors cannot modify hand-written classes, only create new ones.
Yes, Lombok manages to sidestep this limitation, but it does it in a way that
is considered a hack,
which I don’t want Jilt to replicate,
and so it cannot add the toBuilder()
method to the target class.
However, we can employ a simple trick to get around this limitation. We can invert the control flow: instead of making the instance method return a Builder, we can pass that instance to a static factory method of the Builder:
Person person = PersonBuilder.person()
.name("Jack")
// other properties...
.build();
Person copiedPerson = PersonBuilder.toBuilder(person)
.name("John")
.build();
And with that, you can always add a hand-written toBuilder()
method to the target class that delegates to the static one:
public final class Person {
// ...
PersonBuilder toBuilder() {
return PersonBuilder.toBuilder(this);
}
}
In version 1.5.
of Jilt, the static method will be generated on the Builder class when the new toBuilder
attribute of the @Builder
annotation is set.
That attribute allows you to control the name of that generated method.
For the Person
example above, it would look something like:
@Builder(toBuilder = "toBuilder")
public final class Person {
// ...
}
But any valid Java method name can be used, not only "toBuilder"
.
The default value of the toBuilder
attribute is the empty string,
which means this method won’t be generated unless you set the attribute explicitly.
The interesting part is how the generated method initializes the properties of the Builder from the instance of the target class. Jilt uses the following algorithm to find values in the target instance, for each property of the Builder:
- If the target class has a getter for that property (a no-argument method whose name starts with the word “get”, and then the (capitalized) name of the property), use it.
- If the target class has a no-argument method whose name is equal to the name of the property, use it
(this is the case that covers Java 14+
record
types). - Finally, if the previous two steps didn’t find an appropriate method,
assume you can read the field of the target class from the Builder,
with a name identical to the property name
(this is the case when the target class surfaces this property as a
public final
field).
Note that this method doesn’t return a Staged builder,
but a classic one (where you are free to set the properties in any order,
and call the build()
method at any point),
since the assumption is that all properties, including required ones,
have already been set from the provided instance.
Thanks to Alexandre Navarro for submitting this feature request.
Meta-annotations
In some cases, you may want to re-use the same Builder configuration for multiple classes.
For example, you might decide that every value class in your project should use a Staged Builder,
with "set"
as the prefix for setter methods, "create"
as the name of the build method,
and "B_"
as the prefix of the per-property interface names used for the Builder stages.
In such situations, instead of repeating the same annotations in multiple places,
in Jilt 1.5
, you can instead define your own annotation and annotate it with @Builder
and @BuilderInterfaces
:
import org.jilt.Builder;
import org.jilt.BuilderInterfaces;
import org.jilt.BuilderStyle;
@Builder(style = BuilderStyle.STAGED, setterPrefix = "set", buildMethod = "create")
@BuilderInterfaces(innerNames = "B_*")
public @interface MyBuilder {
}
And then, you can place this MyBuilder
so-called meta annotation wherever @Builder
can be placed
(so, a class, constructor, or static method),
and the effect will be as if that element was annotated with the same @Builder
and @BuilderInterfaces
values as @MyBuilder
is annotated with, thus avoiding any duplication in your code:
@MyBuilder // uses @Builder and @BuilderInterfaces values from @MyBuilder
public final class MyValueClass {
// ...
}
Thanks to Diego Pedregal for not only creating this feature request, but going above and beyond, and actually submitting a Pull Request implementing this functionality!
Support for private
constructors
In some cases, you might want to force customers of a class to instantiate it only through its Builder,
and not through any other means, like its constructor, or static factory method.
When writing the Builder code by hand, you would typically achieve this by making the constructor of the class private
,
and making Builder a nested class of the main class.
But, as we already talked above when discussing the toBuilder()
method,
well-behaved annotation processors cannot modify existing classes,
so generating a class nested inside a hand-written class is not possible without Lombok-like hacks.
However, while we cannot generate the nested class,
we can make it easier to write the nested Builder manually.
In the 1.5
version of Jilt, if you place the @Builder
annotation on a private
constructor or static factory method,
the generated code changes: the Builder class becomes abstract,
the fields are protected
instead of private
, and the build()
method is made abstract too.
With this, you can extend the Builder class in a nested class of the main class,
and you only have to override the build()
method to call the private
constructor,
using the fields of the parent Builder class as values of the properties,
to make your Builder class no longer abstract.
You can also provide a static factory method in your class that returns the Builder instance,
conventionally called just builder()
, which allows you to make the nested class private
as well.
For example, if we wanted to make the constructor of the User
class from the
Jilt main ReadMe private
,
it would look something like this:
public final class User {
public final String email, username, firstName, lastName, displayName;
@Builder(style = BuilderStyle.STAGED)
private User(String email, @Opt String username, String firstName,
String lastName, @Opt String displayName) {
this.email = email;
this.username = username == null ? email : username;
this.firstName = firstName;
this.lastName = lastName;
this.displayName = displayName == null
? firstName + " " + lastName
: displayName;
}
private static class InnerBuilder extends UserBuilder {
@Override
public User build() {
return new User(email, username, firstName, lastName, displayName);
}
}
public static UserBuilders.Email builder() {
return new InnerBuilder();
}
}
With the above code, the only way to create an instance of User
would be to use the User.builder()
static method, and then instantiate it through the (Staged, in this case) Builder.
Thanks to Aurélien Mino for submitting this feature request.
JSpecify’s @Nullable
annotations were not recognized
In the previous article
discussing the 1.4
release,
I’ve described how Jilt started automatically treating fields and parameters annotated with
@Nullable
as optional.
The idea was to make this behavior generic,
not tied to any specific @Nullable
annotation,
since there are many competing libraries in this space.
However, it was reported that Jilt was not properly recognizing @Nullable
from one of those libraries,
JSpecify.
As it turns out, JSpecify annotations work a little differently from other entrants in this area,
as they actually belong to the type, instead of to the field or parameter
(more precisely, they belong to the type usage –
the difference between a type declaration and a type usage is most apparent with generic types,
like with List<T>
vs List<String>
).
This issue has been fixed in Jilt 1.5
,
and so now JSpecify annotations are treated the same as other @Nullable
annotations.
Thanks (again) to Alexandre Navarro for
reporting the issue.
Summary
So, those are all the changes included in Jilt release 1.5
.
I’d love any feedback you might have about these, and the library in general.