Credevate di esservi sbarazzati delle Java Pills? E invece eccoci qui con la seconda stagione, pronta a intasare di nuovo le vostre bacheche LinkedIn con l'ennesimo articolo sull'AI! Ma tranquilli: non sarà il solito bla-bla-bla, qui ci si sporca le mani con la tecnica!

Negli ultimi due anni ho sperimentato molto con l'AI, che uso regolarmente nel lavoro. E, sinceramente? È rivoluzionaria. Non è il "futuro", è già il presente, almeno per chi sa usarla, eheh. Quasi un anno fa avevo scritto un articolo su una piccola app per macOS in AppleScript che avevo creato in un pomeriggio interamente con vibe coding: un esempio di come l'AI acceleri la prototipazione.

📌 Lo spike di oggi

Oggi mi sono dedicato un paio d'ore ad uno spike del nostro sprint. Nel nostro progetto usiamo R2DBC per l'accesso al database, e molte query sono complesse e non gestibili con JPA. Inizialmente erano definite in classi dedicate, ma l'obiettivo dello spike era spostarle in file .sql di risorsa e usare DatabaseClient di Spring R2DBC Core.

Una delle complessità? Mappare le risposte del db in oggetti Java senza boilerplate e mantenendo gli oggetti immutabili (niente setter!). Una prima opzione era fare il mapping manuale, ma con decine di oggetti view sarebbe stato lungo, noioso e prono ad errori. La seconda opzione era più elegante, ed è qui che l'AI è entrata in gioco.

📌 La soluzione: BuilderAwareRowMapper

Dopo diverse iterazioni con prompt engineering, è nata la classe BuilderAwareRowMapperR2dbc che:

Il risultato è una classe piccola, chiara ed elegante che gestisce scenari avanzati senza compromettere qualità o regole di progetto.

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); }
    }
}

📌 L'AI è potente, ma serve conoscenza e giudizio

Per risultati ottimali bisogna:

Per chi sa usarla, l'AI può diventare un co-pilot 😛 per sviluppatori, accelerando lo sviluppo senza sostituire responsabilità ed expertise umana.

📑 Link al repo: java-pills (sì, finalmente ho creato il repo pubblico!)

Daje, si riparte carichi con questa nuova stagione! ☕