【问题标题】:How to create an instance of enum using reflection in Java?如何在 Java 中使用反射创建枚举实例?
【发布时间】:2012-03-08 07:37:01
【问题描述】:

在阅读Effective Java时,作者告诉我单元素enum类型是实现单例的最佳方式,因为我们不必考虑复杂的序列化或反射攻击。这意味着我们不能使用反射创建enum 的实例,对吧?

我已经做了一些测试,这里有一个enum 类:

public enum Weekday {}

然后我尝试创建Weekday的实例:

Class<Weekday> weekdayClass = Weekday.class;
Constructor<Weekday> cw = weekdayClass.getConstructor(null);
cw.setAccessible(true);
cw.newInstance(null);

如您所知,它不起作用。当我将关键字enum 更改为class 时,它起作用了。我想知道为什么。谢谢。

【问题讨论】:

  • 您可以使用字节码生成即时创建枚举类。但在这种情况下,枚举在编译时将不存在,因此您将无法在其余代码中引用它(您只能通过反射访问它,这是一个运行-time 而不是编译时机制)。
  • 可以在运行时创建枚举实例,但不能保证这样做总是坏主意——但如果你出于某种奇怪的原因需要这样的破解——你可以在没有热点下所有当前 Java 版本的任何问题。 (您可以在此处查看我的答案以获取更多信息)

标签: java reflection enums


【解决方案1】:

这是语言内置的。来自Java Language Specification (§8.9)

尝试显式实例化枚举类型是编译时错误(第 15.9.1 节)。 Enum 中的 final clone 方法确保永远不会克隆枚举常量,而序列化机制的特殊处理确保永远不会由于反序列化而创建重复的实例。禁止枚举类型的反射实例化。这四件事一起确保枚举类型的实例不存在超出枚举常量定义的实例。

这样做的全部目的是允许安全使用== 来比较Enum 实例。

编辑:请参阅the answer by @GotoFinal,了解如何使用反射打破这种“保证”。

【讨论】:

  • enum 实际上被编译成一个final class,它使用java.lang.Enum 构造函数扩展java.lang.Enum,如果调用setAccessible(true);,这实际上不会阻止反射实例化。我仍然想知道如何防止这种情况发生?
  • @AhmadY.Saleh - 见the answer by soc
  • 虽然您不能通过反射实例化枚举,但您可以通过反射获得枚举,这可能是预期的。示例Enum.valueOf((Class&lt;Enum&gt;) Class.forName("javax.swing.GroupLayout$Alignment"), "CENTER")
  • @Enwired - 是的,您可以通过反射获取enum 的现有实例的引用,但在我看来,OP 想知道为什么新实例可以'不是用那个(或任何其他)机制创建的。
  • @TedHopp 是的,我明白了。但是我来这里是为了寻找一种通过反射获取枚举的方法,所以添加了评论以帮助其他人。 (我的用例是某个 jar 文件是可选的。当它存在时,我想利用其中涉及枚举的特性。)
【解决方案2】:

可以在运行时创建新的枚举实例 - 但这是一个非常糟糕的主意,并且可能会在任何更新中中断。您可以为此使用不安全或反射。

就像这个例子中的枚举:

public enum Monster {
    ZOMBIE(Zombie.class, "zombie"),
    ORK(Ork.class, "ork"),
    WOLF(Wolf.class, "wolf");
    private final Class<? extends Entity> entityClass;
    private final String                  entityId;
    Monster(Class<? extends Entity> entityClass, String entityId) {
        this.entityClass = entityClass;
        this.entityId = "monster:" + entityId;
    }
    public Class<? extends Entity> getEntityClass() { return this.entityClass; }
    public String getEntityId() { return this.entityId; }
    public Entity create() {
        try { return entityClass.newInstance(); }
        catch (InstantiationException | IllegalAccessException e) { throw new InternalError(e); }
    }
}

我们可以使用

Class<Monster> monsterClass = Monster.class;
// first we need to find our constructor, and make it accessible
Constructor<?> constructor = monsterClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);

// this is this same code as in constructor.newInstance, but we just skipped all that useless enum checks ;)
Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
constructorAccessorField.setAccessible(true);
// sun.reflect.ConstructorAccessor -> internal class, we should not use it, if you need use it, it would be better to actually not import it, but use it only via reflections. (as package may change, and will in java 9+)
ConstructorAccessor ca = (ConstructorAccessor) constructorAccessorField.get(constructor);
if (ca == null) {
    Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
    acquireConstructorAccessorMethod.setAccessible(true);
    ca = (ConstructorAccessor) acquireConstructorAccessorMethod.invoke(constructor);
}
// note that real constructor contains 2 additional parameters, name and ordinal
Monster enumValue = (Monster) ca.newInstance(new Object[]{"CAERBANNOG_RABBIT", 4, CaerbannogRabbit.class, "caerbannograbbit"});// you can call that using reflections too, reflecting reflections are best part of java ;)

在 java 9 上,由于使用了内部类,这可能无法编译,正如我在评论中描述的那样 - 您可以使用不安全甚至更多的反射来跳过它。

但是我们还需要将该常量添加到 enum 本身,因此 Enum.values() 将返回有效列表,我们可以通过使用旧技巧更改 final 字段的值来使 final 字段再次成为 non-final 来做到这一点:

static void makeAccessible(Field field) throws Exception {
    field.setAccessible(true);
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
}

然后将该字段更改为包含我们的新字段的新值:

Field valuesField = Monster.class.getDeclaredField("$VALUES");
makeAccessible(valuesField);
// just copy old values to new array and add our new field.
Monster[] oldValues = (Monster[]) valuesField.get(null);
Monster[] newValues = new Monster[oldValues.length + 1];
System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
newValues[oldValues.length] = enumValue;
valuesField.set(null, newValues);

还有另一个存储枚举常量的字段,所以对它做类似的技巧也很重要: private volatile transient T[] enumConstants = null; - 在Class.class 中,注意它可以为null - java 将在下次使用时重新生成它们。
private volatile transient Map&lt;String, T&gt; enumConstantDirectory = null; - 在Class.class,注意它也可以为null,与上面的字段相同。

因此,只需使用反射将它们设置为 null,您的新值就可以使用了。
如果不使用检测或其他技巧编辑类,唯一不可能的事情是为该枚举添加真实字段以获得我们的新值。

还可以使用 Unsafe 类创建新的枚举实例:

public static void unsafeWay() throws Throwable {
    Constructor<?> constructor = Unsafe.class.getDeclaredConstructors()[0];
    constructor.setAccessible(true);
    Unsafe unsafe = (Unsafe) constructor.newInstance();
    Monster enumValue = (Monster) unsafe.allocateInstance(Monster.class);
}

但是unsafe类不调用构造函数,所以需要手动初始化所​​有字段...

Field ordinalField = Enum.class.getDeclaredField("ordinal");
makeAccessible(ordinalField);
ordinalField.setInt(enumValue, 5);

Field nameField = Enum.class.getDeclaredField("name");
makeAccessible(nameField);
nameField.set(enumValue, "LION");

Field entityClassField = Monster.class.getDeclaredField("entityClass");
makeAccessible(entityClassField);
entityClassField.set(enumValue, Lion.class);

Field entityIdField = Monster.class.getDeclaredField("entityId");
makeAccessible(entityIdField);
entityIdField.set(enumValue, "Lion");

请注意,您还需要初始化内部枚举字段。
同样使用 unsafe 应该可以声明新类来创建抽象枚举类的新实例。我使用 javassist 库来减少生成新类所需的代码:

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(MyEnum.VALUE.getSomething());

        ClassPool classPool = ClassPool.getDefault();
        CtClass enumCtClass = classPool.getCtClass(MyEnum.class.getName());
        CtClass ctClass = classPool.makeClass("com.example.demo.MyEnum$2", enumCtClass);

        CtMethod getSomethingCtMethod = new CtMethod(CtClass.intType, "getSomething", new CtClass[0], ctClass);
        getSomethingCtMethod.setBody("{return 3;}");
        ctClass.addMethod(getSomethingCtMethod);

        Constructor<?> unsafeConstructor = Unsafe.class.getDeclaredConstructors()[0];
        unsafeConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeConstructor.newInstance();

        MyEnum newInstance = (MyEnum) unsafe.allocateInstance(ctClass.toClass());
        Field singletonInstance = MyEnum.class.getDeclaredField("VALUE");
        makeAccessible(singletonInstance);
        singletonInstance.set(null, newInstance);

        System.out.println(MyEnum.VALUE.getSomething());
    }

    static void makeAccessible(Field field) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
    }
}

enum MyEnum {
    VALUE {
        @Override
        public int getSomething() {
            return 5;
        }
    };

    public abstract int getSomething();
}

这将打印 5 然后 3。请注意,这是不可能枚举不包含子类的类 - 因此没有任何覆盖的方法,因为枚举被声明为最终类。

来源:https://blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html

【讨论】:

  • 这真是令人兴奋。我还没有玩过这个,但我的第一印象是你在运行时用一个新值扩展了Monster 枚举,然后创建了该值的一个实例。可以使用此方法创建现有枚举值的第二个实例吗?如果是这样,我想知道这可能对安全造成什么影响。
  • @TedHopp 你的意思是修改一些枚举常量?它只是一个字段,所以很简单,您可以在我的回答中看到示例:stackoverflow.com/a/51246456/4378853 安全隐患?如果您可以访问代码,那么您可以做任何您想做的事情,那么就没有安全性这样的东西。除非您正在考虑在安全管理器下执行外部 java 代码 - 那么您总是需要阻止反射或安全管理器是无用的。 (
  • 我的意思是创建特定枚举值的第二个实例。挑战在于有两个变量ab,类型为MONSTER,这样a.name().equals(b.name()) 的计算结果为true,但a == b 的计算结果为false
  • @TedHopp 没问题,您可以创建重复的名称/序号 - 您甚至可以通过修改缓存值来覆盖 Enum.valueOf 应该返回的内容。所以你可以让MyEnum.INST != MyEnum.valueOf(MyEnum.INST.name())
  • 我的猜测是,Java 人员最终会尝试阻止这种方法来混淆枚举语义。同时,恭喜您完成了这一切。
【解决方案3】:

这可能会恢复死帖,但您可以获取使用Weekday.class.getEnumConstants() 声明的每个常量的实例。这将返回一个包含所有常量的数组,其中获取单个实例很简单,getEnumConstants()[0]

【讨论】:

    【解决方案4】:

    因此,如果您的目标是持久化然后重建枚举信息。您需要保留 enumClassName 及其值。

    public enum DaysOfWeek{ Mon, Tue, Wed, Thu, Fri, Sat, Sun }
    
    DaysOfWeek dow = DaysOfWeek.Tue;
    String value = dow.toString();
    String enumClassName = dow.getClass().getName();
    
    // Persist value and enumClassName
    // ...
    
    // Reconstitute the data 
    Class clz = Class.forName(enumClassName);
    Object o = Enum.valueOf(clz, value);
    DaysOfWeek dow2 = (DaysOfWeek)o;
    System.out.println(dow2);
    

    【讨论】:

      【解决方案5】:

      不能追溯创建枚举类的新实例是正确的,即使使用反射也不行。

      以下代码演示了这一点:

      val weekdayClass = classOf[Weekday]
      val weekdayConstructor = weekdayClass getDeclaredConstructor (classOf[String], classOf[Int])
      weekdayConstructor setAccessible true
      weekdayConstructor newInstance ("", Integer.valueOf(0))
      

      通常,这应该有效。但在枚举的情况下,这是Constructor#newInstance 中的特殊情况:

      if ((clazz.getModifiers() & Modifier.ENUM) != 0)
          throw new IllegalArgumentException("Cannot reflectively create enum objects");
      

      因此,我们在尝试实例化新枚举实例时收到以下异常:

      java.lang.IllegalArgumentException: Cannot reflectively create enum objects
              at java.lang.reflect.Constructor.newInstance(Constructor.java:520)
              ...
      

      我假设最后一种方法(可能会成功,因为没有运行检查或构造函数)涉及sun.misc.Unsafe#allocateInstance

      【讨论】:

        【解决方案6】:

        枚举被设计为被视为常量对象。它覆盖 readObject 并抛出无效对象异常以防止默认序列化。它还覆盖 clone() 并抛出不支持克隆的异常。就反射而言,Enum的构造函数是受保护的。所以如果你使用上面的代码,它会抛出NoSuchMethodFound。

        即使您使用 getDeclaredConstructor() 而不是 getConstructor,您也应该得到相同的异常。我假设它是通过 java 中的 SecurityManager 限制的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-12
          • 2017-10-01
          相关资源
          最近更新 更多