Philipp
Philipp

Reputation: 420

Automatically generate code to dynamically create Java objects (Reflection)

I have classes which are automatically created from a JSON schema. In my Example one of these classes is called AvroPropertie.

Declaring and initializing objects from this class looks like this:

AvroPropertie aPropertie = AvroPropertie.newBuilder()
                    .setName(properties.get("name").asText())
                    .setDate(properties.get("date").asText())
                    .build();

If the JSON schema differs the variables like name and date differ as well. There could be more, less or totally different ones. It's always only these setters and the build() method at the end.

Is there a way to generate this code automatically? Maybe reflection?

More context: I use the ApacheAvro Serializing and deserializing with code generation.

Ty!

Upvotes: 1

Views: 2401

Answers (1)

Michał Ziober
Michał Ziober

Reputation: 38635

Using Reflection you can:

  1. Invoke static newBuilder method to create builder.
  2. Find all fields/setters for given POJO class. If in all cases setters on builder and setters on POJO have the same name like in your example it should be easy to convert.
  3. Invoke build on builder instance to create new POJO.

Using basic Java Reflection and Stream API-s it could look like below:

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Properties;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Stream;

public class ReflectionApp {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        properties.put("name", "John");
        properties.put("date", "today");

        AvroPropertie manualInstance = AvroPropertie.newBuilder()
                .setName(properties.getProperty("name"))
                .setDate(properties.getProperty("date"))
                .build();

        Object dynamicInstance = AvroAutoCoder.createAndSet(AvroPropertie.class, properties::getProperty);

        System.out.println(manualInstance);
        System.out.println(dynamicInstance);
        System.out.println("manualInstance == dynamicInstance => " + manualInstance.equals(dynamicInstance));
    }
}

class AvroAutoCoder {

    public static Object createAndSet(Class clazz, Function<String, String> dataSupplier) throws Exception {
        Object builderInstance = findMethod(clazz, "newBuilder")
                .invoke(null);

        Class<?> builderClass = builderInstance.getClass();
        getSetters(clazz).forEach(setter -> {
            try {
                String fieldName = setter.getName().substring(3).toLowerCase();
                findMethod(builderClass, setter.getName())
                        .invoke(builderInstance, dataSupplier.apply(fieldName));
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        });

        return findMethod(builderClass, "build")
                .invoke(builderInstance);
    }

    private static Method findMethod(Class clazz, String methodName) {
        return Arrays.stream(clazz.getDeclaredMethods())
                .filter(method -> method.getName().equals(methodName))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }

    private static Stream<Method> getSetters(Class clazz) {
        return Arrays.stream(clazz.getDeclaredMethods())
                .filter(method -> method.getParameterCount() == 1 && method.getReturnType() == Void.TYPE)
                .filter(method -> method.getName().startsWith("set"));
    }
}

Above code prints:

AvroPropertie[name='John', date='today']
AvroPropertie[name='John', date='today']
manualInstance == dynamicInstance => true

Comment added by the Author of question:

In this specific example class APropertie has a static inner class named Builder which holds the wanted setters. In this case the getSetters() methods needs to be changed a little bit:

// Find all setter of class
private static Stream<Method> getSetters2(Class clazz) {
    Optional<Class> first = Arrays.stream(clazz.getDeclaredClasses())
            .findFirst();

    return Arrays.stream(first.get().getDeclaredMethods())
            .filter(method -> method.getName().startsWith("set"));
}

Upvotes: 1

Related Questions