1

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 ?

Thoomas
  • 2,150
  • 2
  • 19
  • 33
  • 6
    If its dynamic then Enum kind of loses its charm. Its just a basic object at this point. Might as well use a class for this. – Sujoy Jan 17 '22 at 17:52
  • I can't agree more. Unfortunately, this is legacy code and the application relies a lot on it. It populates hundreds of enums. – Thoomas Jan 17 '22 at 17:54
  • 2
    How about generating the enum classes at runtime, generating bytecodes rather than using reflection? There are libraries for that and it should work with Java11+. – ewramner Jan 17 '22 at 18:01
  • 1
    Sounds like you cannot postpone your technical debt using `sun.*` classes any longer. – Thorbjørn Ravn Andersen Jan 17 '22 at 18:49
  • Since your task is to make it work in Java 11, this seems like a good opportunity to change the application from creating dynamic enums (which, as Sujoy points out, defeats the point of using enums) to using a (probably static) `Map`. – VGR Jan 18 '22 at 13:59
  • Thanks to all for your feedbacks. I'll try to change to something using a `Map`. I'll post it as an answer later (in case someone will have to deal with the same issue). – Thoomas Jan 18 '22 at 14:50

2 Answers2

5

This answer contains a working approach which uses only the standard API and still works, even with JDK 17.

Since it uses a JDK type for the example, which requires an --add-opens java.base/java.lang=… argument at startup time, here’s an example using its own enum type which does not require any modifications to the environment.

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.util.EnumSet;

class EnumHack {
    public static void main(String[] args) throws Throwable {
        System.out.println(Runtime.version());
        Constructor<Example> c
            = Example.class.getDeclaredConstructor(String.class, int.class);
        c.setAccessible(true);
        MethodHandle h = MethodHandles.lookup().unreflectConstructor(c);
        Example baz = (Example) h.invokeExact("BAZ", 42);
        System.out.println("created Example " + baz + "(" + baz.ordinal() + ')');
        EnumSet<Example> set = EnumSet.allOf(Example.class);
        System.out.println(set.contains(baz));
        set.add(baz);
        System.out.println(set);
    }

    enum Example {
        FOO, BAR
    }
}

Since it doesn’t require special setup, it can be demonstrated on Ideone

12.0.1+12
created Example BAZ(42)
false

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 42 out of bounds for length 2
    at java.base/java.util.RegularEnumSet$EnumSetIterator.next(RegularEnumSet.java:105)
    at java.base/java.util.RegularEnumSet$EnumSetIterator.next(RegularEnumSet.java:78)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:472)
    at java.base/java.lang.String.valueOf(String.java:3042)
    at java.base/java.io.PrintStream.println(PrintStream.java:897)
    at EnumHack.main(Main.java:18)

This not only shows that the hack does its job, but also some of the problems cause by this. The set supposed to contain all elements doesn’t contain the new constant and after adding it manually, the resulting inconsistent state produces exceptions on subsequent operations.

So, such enum constant must not be used with the standard tools for enum types, which matches what has been said in the question’s comments, it loses the advantage of being an enum type. In fact, it’s worse than that.

So the approach shown above should only be used as a temporary work-around, or not at all.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Note that this only works because `EnumHack` and `Example` are nestmates. If the enum is in a different nest, you may need `privateLookupIn` or something similar. – Johannes Kuhn Jan 19 '22 at 18:28
  • 2
    @JohannesKuhn the example uses `setAccessible(true)` which has a similar effect to `privateLookupIn` – Holger Jan 19 '22 at 18:34
-1

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.* !

Thoomas
  • 2,150
  • 2
  • 19
  • 33