Did you think you'd got rid of the Java Pills? Well, here we are with the second season, ready to clog up your LinkedIn feeds again with yet another article about AI! But don't worry: it won't be the usual blah-blah-blah, here we get our hands dirty with the technical stuff!

Over the last two years I've experimented a lot with AI, which I use regularly at work. And, honestly? It's revolutionary. It's not the "future", it's already the present, at least for those who know how to use it, heh. Almost a year ago I'd written an article about a small macOS app in AppleScript that I'd created in one afternoon entirely with vibe coding: an example of how AI speeds up prototyping.

πŸ“Œ Today's spike

Today I spent a couple of hours on a spike from our sprint. In our project we use R2DBC for database access, and many queries are complex and can't be handled with JPA. They were initially defined in dedicated classes, but the goal of the spike was to move them into .sql resource files and use the DatabaseClient from Spring R2DBC Core.

One of the complexities? Mapping the db responses onto Java objects without boilerplate and keeping the objects immutable (no setters!). A first option was to do the mapping manually, but with dozens of view objects it would have been long, tedious and error-prone. The second option was more elegant, and this is where AI came into play.

πŸ“Œ The solution: BuilderAwareRowMapper

After several iterations with prompt engineering, the BuilderAwareRowMapperR2dbc class was born, which:

The result is a small, clear and elegant class that handles advanced scenarios without compromising quality or project rules.

public class BuilderAwareRowMapperR2dbc<T> {

    private final Class<T> targetClass;
    private final Map<String, Method> methodCache = new HashMap<>();
    private static final ConversionService CONVERSION_SERVICE = DefaultConversionService.getSharedInstance();

    private BuilderAwareRowMapperR2dbc(final Class<T> targetClass) { this.targetClass = targetClass; }

    public static <T> BuilderAwareRowMapperR2dbc<T> of(final Class<T> targetClass) {
        return new BuilderAwareRowMapperR2dbc<>(targetClass);
    }

    @SuppressWarnings("unchecked")
    public T map(final Row row, final RowMetadata meta) {
        if (!targetClass.isAnnotationPresent(BuilderMappable.class)) {
            throw new BuilderMappingException("Class " + targetClass.getSimpleName() + " is not BuilderMappable");
        }
        try {
            final Object builder = targetClass.getMethod("builder").invoke(null);
            for (final ColumnMetadata columnMetadata : meta.getColumnMetadatas()) {
                final String columnName = columnMetadata.getName();
                final String propertyName = toCamelCase(columnName);
                setIfExists(builder, propertyName, row.get(columnName));
            }
            return (T) builder.getClass().getMethod("build").invoke(builder);
        } catch (final Exception e) {
            throw new BuilderMappingException("Error mapping BuilderAwareRowMapper for " + targetClass.getSimpleName(), e);
        }
    }

    private void setIfExists(final Object builder, final String name, final Object value) {
        try {
            final Method method = methodCache.computeIfAbsent(name, n -> findMethod(builder, n));
            final Object converted = CONVERSION_SERVICE.convert(value, method.getParameterTypes()[0]);
            if (converted != null) { method.invoke(builder, converted); }
        } catch (final Exception ignored) { /* ignore missing conversion */ }
    }

    private Method findMethod(final Object builder, final String name) {
        for (final Method method : builder.getClass().getMethods()) {
            if (method.getName().equals(name) && method.getParameterCount() == 1) {
                return method;
            }
        }
        return null;
    }

    private String toCamelCase(final String input) {
        return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, input.toLowerCase());
    }

    public static class BuilderMappingException extends RuntimeException {
        public BuilderMappingException(final String message) { super(message); }
        public BuilderMappingException(final String message, final Throwable cause) { super(message, cause); }
    }
}

πŸ“Œ AI is powerful, but it takes knowledge and judgement

For optimal results you need to:

For those who know how to use it, AI can become a co-pilot πŸ˜› for developers, speeding up development without replacing human responsibility and expertise.

πŸ“‘ Link to the repo: java-pills (yes, I've finally created the public repo!)

Come on, we're back and fired up for this new season! β˜•