使用反射的 OpenJDK 11 动态枚举
Dynamic enum with OpenJDK 11 using reflection
我正在开发一个项目 运行 JDK8,我们想将它迁移到 OpenJDK11。
但是,有遗留代码在运行时动态创建枚举(使用反射和 sun.reflect.*
包):
public class EnumUtil {
static Object makeEnum(...) {
...
enumClass.cast(sun.reflect.ReflectionFactory.getReflectionFactory() .newConstructorAccessor(constructor).newInstance(params));
}
}
或
// before, field is made accessible, the modifier too
sun.reflect.FieldAccessor fieldAccessor = sun.reflect.ReflectionFactory.getReflectionFactory().newFieldAccessor(field, false);
field.set(target, value);
例如,假设我们有枚举 AEnum
:
public enum AEnum {
; // no values at compile time
private String label;
private AEnum (String label) {
this.label = label;
}
然后,我们像这样添加枚举值:
EnumUtil.addEnum(MyEnum.class, "TEST", "labelTest");
最后,在运行时,我们有一个值 AEnum.TEST
(不是直接调用,而是 Enum.valueOf
),标签 = labelTest。
很遗憾,sun.reflect.*
类 在 OpenJDK11 中不再可用。
我试过使用 jdk.internal.reflect.ConstructorAccessor
,但出现错误 java: package jdk.internal.reflect does not exist
。而且我不认为依赖 jdk.internal.*
类.
是个好主意
是否有任何 OpenJDK11 替代方案可以在运行时创建枚举?
This answer 包含一种仅使用标准 API 并且仍然有效的工作方法,即使使用 JDK 17.
因为它使用 JDK 类型作为例子,在启动时需要一个 --add-opens java.base/java.lang=…
参数,下面是一个使用它自己的 enum
类型的例子,不需要任何修改对环境。
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
}
}
由于不需要特殊设置,可以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)
这不仅说明 hack 做了它的工作,而且还说明了由此引起的一些问题。本应包含所有元素的集合不包含新常量,手动添加后,产生的不一致状态会在后续操作中产生异常。
因此,这种枚举常量一定不能与枚举类型的标准工具一起使用,这与问题评论中所说的相符,它失去了作为枚举类型的优势。事实上,它比那更糟。
所以上面显示的方法只能用作临时解决方法,或者根本不用。
正如你们在评论部分提到的那样,在运行时添加 enum
值是一个非常糟糕的主意,破坏了枚举的契约。
所以,我已经将所有案例修改为一个 POJO 对象,维护了一个映射。
在简化格式中,AEnum
变为:
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;
}
}
我们从数据库中读取动态值,所以我制作了一个实用程序 class 来加载所有内容。
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) {
// ...
}
}
然后:
addEnum(AEnum.class, "TEST", "labelTest");
addEnum(AEnum.class, "TEST2", "labelTest2");
AEnum.getAll() // returns a list with the two entries
此外,如果我们在持久的 Entity
中使用这个“假枚举”,我们有一个转换器来管理 String
和 AEnum
之间的转换。
@Entity
@Table(name = TABLE_NAME)
...
public class MyEntity {
@Column(name = COLUMN_TYPE)
@Convert(converter = AEnumConverter.class)
private AEnum type;
AEnumConverter
实现 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);
}
}
有了这个机制,一切都很完美,我们不再需要sun.reflect.*
!
我正在开发一个项目 运行 JDK8,我们想将它迁移到 OpenJDK11。
但是,有遗留代码在运行时动态创建枚举(使用反射和 sun.reflect.*
包):
public class EnumUtil {
static Object makeEnum(...) {
...
enumClass.cast(sun.reflect.ReflectionFactory.getReflectionFactory() .newConstructorAccessor(constructor).newInstance(params));
}
}
或
// before, field is made accessible, the modifier too
sun.reflect.FieldAccessor fieldAccessor = sun.reflect.ReflectionFactory.getReflectionFactory().newFieldAccessor(field, false);
field.set(target, value);
例如,假设我们有枚举 AEnum
:
public enum AEnum {
; // no values at compile time
private String label;
private AEnum (String label) {
this.label = label;
}
然后,我们像这样添加枚举值:
EnumUtil.addEnum(MyEnum.class, "TEST", "labelTest");
最后,在运行时,我们有一个值 AEnum.TEST
(不是直接调用,而是 Enum.valueOf
),标签 = labelTest。
很遗憾,sun.reflect.*
类 在 OpenJDK11 中不再可用。
我试过使用 jdk.internal.reflect.ConstructorAccessor
,但出现错误 java: package jdk.internal.reflect does not exist
。而且我不认为依赖 jdk.internal.*
类.
是否有任何 OpenJDK11 替代方案可以在运行时创建枚举?
This answer 包含一种仅使用标准 API 并且仍然有效的工作方法,即使使用 JDK 17.
因为它使用 JDK 类型作为例子,在启动时需要一个 --add-opens java.base/java.lang=…
参数,下面是一个使用它自己的 enum
类型的例子,不需要任何修改对环境。
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
}
}
由于不需要特殊设置,可以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)
这不仅说明 hack 做了它的工作,而且还说明了由此引起的一些问题。本应包含所有元素的集合不包含新常量,手动添加后,产生的不一致状态会在后续操作中产生异常。
因此,这种枚举常量一定不能与枚举类型的标准工具一起使用,这与问题评论中所说的相符,它失去了作为枚举类型的优势。事实上,它比那更糟。
所以上面显示的方法只能用作临时解决方法,或者根本不用。
正如你们在评论部分提到的那样,在运行时添加 enum
值是一个非常糟糕的主意,破坏了枚举的契约。
所以,我已经将所有案例修改为一个 POJO 对象,维护了一个映射。
在简化格式中,AEnum
变为:
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;
}
}
我们从数据库中读取动态值,所以我制作了一个实用程序 class 来加载所有内容。
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) {
// ...
}
}
然后:
addEnum(AEnum.class, "TEST", "labelTest");
addEnum(AEnum.class, "TEST2", "labelTest2");
AEnum.getAll() // returns a list with the two entries
此外,如果我们在持久的 Entity
中使用这个“假枚举”,我们有一个转换器来管理 String
和 AEnum
之间的转换。
@Entity
@Table(name = TABLE_NAME)
...
public class MyEntity {
@Column(name = COLUMN_TYPE)
@Convert(converter = AEnumConverter.class)
private AEnum type;
AEnumConverter
实现 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);
}
}
有了这个机制,一切都很完美,我们不再需要sun.reflect.*
!