When you’ve read a few of my earlier blog posts, you know that I cherish a secret love for the builder pattern used to create immutable objects. My default implementation includes the Jackson library for (de)serialization of this pattern, and Lombok for providing the perfect glue with almost no boilerplate code.
When you’ve not read any of my earlier posts (yes, shame on you) here is the link: What I love about Lombok and the builder pattern
Shortcut
Only interested in the code? Look at the final gist.
What’s the problem?
When using the builder pattern with Jackson in a regular Java Virtual Machine (JVM) environment, everything is fine, everything works fine because it allows for reflection during runtime.
When using native compilation, which was recently released in Spring Boot 3 (link), you cannot rely on this reflection during runtime and need to configure it upfront during build time.
When you do not configure anything during build time, you will run into a RuntimeException that looks something like this.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Builder class nl.vreijsenj.blog.Movie$Builder does not have build method (name: 'build')
Native Configuration
Spring Boot 3 allows for what’s called “native hints” to be declared, which are used during build time. These Native Hints allow you to specify classes or methods that should be accessible using reflection, or specify resources that would normally be excluded by the native build but are actually needed for a specific use-case.
Spring Boot’s way of specifying these native hints is by implementing the RuntimeHints API.
Identifying Jackson Builders
We first need to be able to identify the builder classes that we need to configure reflection for.
As you know, Lombok’s @Jacksonized
annotation will put Jackson’s @JsonPOJOBuilder
annotations on the builder class which we can use to identify our builder classes.
Let’s see how that would look;
List builders = getClasses(loader, ROOT_PACKAGE).stream()
.filter(clazz -> clazz.getAnnotationsByType(JsonPOJOBuilder.class).length > 0)
.map(TypeReference::of)
.toList();
We then have to instruct the native compilation process to allow invocation of the declared constructor and public methods, think about the build method from our error message.
hints
.reflection()
.registerTypes(builders, TypeHint.builtWith(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS))
Completing the puzzle
When we put the above into our own implementation of a RuntimeHintsRegistrar
we get the following class:
@Configuration
@ImportRuntimeHints(JacksonHints.class)
public class JacksonHints implements RuntimeHintsRegistrar {
private static final String ROOT_PACKAGE = "nl.vreijsenj.streaming";
private static final String PACKAGE_SEPARATOR = ".";
private static final String FOLDER_SEPARATOR = "/";
@Override
public void registerHints(RuntimeHints hints, ClassLoader loader) {
List builders = getClasses(loader, ROOT_PACKAGE).stream()
.filter(clazz -> clazz.getAnnotationsByType(JsonPOJOBuilder.class).length > 0)
.map(TypeReference::of)
.toList();
hints
.reflection()
.registerTypes(builders, TypeHint.builtWith(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS))
}
public Set> getClasses(ClassLoader loader, String name) {
InputStream stream = loader.getResourceAsStream(
name.replaceAll("[" + PACKAGE_SEPARATOR + "]", FOLDER_SEPARATOR)
);
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
List lines = reader.lines().toList();
Stream> current = lines.stream()
.filter(this::isClassFile)
.map(line -> getClass(line, name));
Stream> nested = lines.stream()
.filter(this::isPackageFolder)
.map(String::trim)
.map(child -> setChildPackageName(name, child))
.map(pName -> getClasses(loader, pName))
.flatMap(Set::stream);
return Stream.concat(current, nested).collect(Collectors.toSet());
}
@SneakyThrows
private Class> getClass(String className, String packageName) {
return Class.forName(packageName + PACKAGE_SEPARATOR + className.substring(0, className.lastIndexOf(PACKAGE_SEPARATOR)));
}
private boolean isClassFile(String path) {
return path.endsWith(".class");
}
private boolean isPackageFolder(String path) {
return ! isClassFile(path);
}
private String setChildPackageName(String parent, String child) {
return parent + PACKAGE_SEPARATOR + child;
}
}
Summary
Well, there you have it, a class ready to be included in any Spring Boot 3 application. Rather prefer a gist? Way ahead of you here is the gist.
That’s all folks! 👋