Search code examples
javareflectionenumsopenjdk-11

Dynamic enum with OpenJDK 11 using reflection


I'm working on a project running with JDK8 and we want to migrate it to OpenJDK11.

But, there is legacy code that creates enums dynamically at runtime (using reflection and sun.reflect.* packages) :

public class EnumUtil {
    static Object makeEnum(...) {
        ...
        enumClass.cast(sun.reflect.ReflectionFactory.getReflectionFactory() .newConstructorAccessor(constructor).newInstance(params));
    }
}

Or

    // before, field is made accessible, the modifier too
    sun.reflect.FieldAccessor fieldAccessor = sun.reflect.ReflectionFactory.getReflectionFactory().newFieldAccessor(field, false);
    field.set(target, value);

For example, let's say we have the enum AEnum :

public enum AEnum {
    ; // no values at compile time

    private String label;

    private AEnum (String label) {
        this.label = label;
    }

Then, we add enum values like this :

EnumUtil.addEnum(MyEnum.class, "TEST", "labelTest");

Finally, we have, at runtime, a value AEnum.TEST (not with that direct call, but with Enum.valueOf) with the label = labelTest.

Unfortunately, sun.reflect.* classes are no longer available in OpenJDK11.

I've tried using jdk.internal.reflect.ConstructorAccessor but I'm getting the error java: package jdk.internal.reflect does not exist. And I don't think it's a good idea to rely on jdk.internal.* classes.

Is there any OpenJDK11 alternative to create enums at runtime ?


Solution

  • As you all mentionned in the comments' section, adding enum values at runtime is a very bad idea, breaking the contract of an enum.

    So, I've modified all the cases into a POJO object, maintaining a map.

    In a simplified format, AEnum becomes :

    public class AEnum extends DynamicEnum { // DynamicEnum has utility methods in order to act as close as a real enum.
        @Getter
        private String label;
    
        private static final Map<String, AEnum> map = new LinkedHashMap<>();
    
        protected AEnum (String name, String label) {
            this.name = name;
            this.label= label;
        }
    
        public static AEnum addInMap(String name, String label) {
            AEnum value = new DossierSousType(name, label);
            map.put(name, value);
            return value;
        }
    }
    

    We read the dynamic values from the database, so I've made an utility class to load everything.

    public static <T extends DynamicEnum> T addEnum(final Class<T> type, final String name, final String label) throws TechnicalException {
        try {
            Method method = type.getDeclaredMethod("addInMap", String.class, String.class);
            return (T) method.invoke(null, name, label);
        } catch (... e) {
            // ...
        }
    }
    

    Then :

    addEnum(AEnum.class, "TEST", "labelTest");
    addEnum(AEnum.class, "TEST2", "labelTest2");
    AEnum.getAll() // returns a list with the two entries
    

    Moreover, if we use this "false enum" inside a persisted Entity, we have a converter to manage conversion between String and AEnum.

    @Entity
    @Table(name = TABLE_NAME)
    ...
    public class MyEntity {
        @Column(name = COLUMN_TYPE)
        @Convert(converter = AEnumConverter.class)
        private AEnum type;
    

    AEnumConverter implements javax.persistence.AttributeConverter :

    @Converter
    public class AEnumConverter implements AttributeConverter<AEnum , String> {
    
        @Override
        public String convertToDatabaseColumn(AEnum type) {
            return type != null ? type.getName() : null;
        }
    
        @Override
        public AEnum convertToEntityAttribute(String type) {
            return AEnum .getEnum(type);
        }
    }
    

    With this mechanism, everything works perfectly, and we no longer need sun.reflect.* !