在Java中是否可以更改或修改枚举本身,从而破坏枚举单例?

是否有可能以某种方式在运行时更改枚举? 例如使用reflection。 问题不是要改变枚举常量的状态。 它将要更改枚举的常量集或删除任何常量。

关于以下枚举,是否可以添加颜色WHITE或删除颜色RED或更改其顺序?

 public enum Color { RED, GREEN, BLUE; } 

为什么我问?

  • 首先,我想知道它是否可行。
  • 但如果是的话,它会对使用Enum实现单例的常见方式产生影响吗?

我知道这个问题有点恶意。 但即使是约书亚布洛赫在谈论(1)关于实施单身人士并推荐了枚举单身人士模式时也提到了“ 巧妙制造的攻击 ”。 如果我们可以修改枚举,那么对这种模式的攻击是否可行?

我试图解决它并部分管理它。 我将发布我的结果作为答案 – 遵循这个建议 。


(1)请参阅在Java中实现单例模式的有效方法是什么? 其中包含指向effective_java_reloaded.pdf的链接,第31页。

我开始分析使用javap -c反汇编枚举Color 。 以下摘录自:

  static {}; Code: 0: new #1 // class playground/Color 3: dup 4: ldc #14 // String RED 6: iconst_0 7: invokespecial #15 // Method "":(Ljava/lang/String;I)V 10: putstatic #19 // Field RED:Lplayground/Color; 13: new #1 // class playground/Color 16: dup 17: ldc #21 // String GREEN 19: iconst_1 20: invokespecial #15 // Method "":(Ljava/lang/String;I)V 23: putstatic #22 // Field GREEN:Lplayground/Color; 26: new #1 // class playground/Color 29: dup 30: ldc #24 // String BLUE 32: iconst_2 33: invokespecial #15 // Method "":(Ljava/lang/String;I)V 36: putstatic #25 // Field BLUE:Lplayground/Color; 39: iconst_3 40: anewarray #1 // class playground/Color 43: dup 44: iconst_0 45: getstatic #19 // Field RED:Lplayground/Color; 48: aastore 49: dup 50: iconst_1 51: getstatic #22 // Field GREEN:Lplayground/Color; 54: aastore 55: dup 56: iconst_2 57: getstatic #25 // Field BLUE:Lplayground/Color; 60: aastore 61: putstatic #27 // Field ENUM$VALUES:[Lplayground/Color; 64: return 

在索引61处,我们看到三个枚举常量被分配给名为ENUM$VALUES的静态数组。

通过reflection列出所有静态字段……

 Field[] declaredFields = Color.class.getDeclaredFields(); for (Field field : declaredFields) { if (Modifier.isStatic(field.getModifiers())) { System.out.println(field.getName() + ": " + field.getType()); } } 

显示枚举常量并显示数组:

 RED: class playground.ReflectEnum$Color GREEN: class playground.ReflectEnum$Color BLUE: class playground.ReflectEnum$Color ENUM$VALUES: class [Lplayground.ReflectEnum$Color; 

我定义了以下方法来获取枚举数组:

  protected static > E[] getEnumsArray(Class ec) throws Exception { Field field = ec.getDeclaredField("ENUM$VALUES"); field.setAccessible(true); return (E[]) field.get(ec); } 

使用它可以改变枚举常量的顺序:

 Color[] colors = getEnumsArray(Color.class); colors[0] = Color.GREEN; colors[1] = Color.RED; colors[2] = Color.BLUE; 

列出枚举常量

 for (Color color : Color.values()) { System.out.println(action + ":" + color.ordinal()); } 

说明:

 GREEN:1 RED:0 BLUE:2 

显然订单已经改变了。

由于可以为数组赋值,因此分配null也是有效的。

 Color[] colors = getEnumsArray(Color.class); colors[0] = Color.GREEN; colors[1] = Color.RED; colors[2] = null; 

列出枚举常量显示:

 GREEN:1 RED:0 Exception in thread "main" java.lang.NullPointerException at playground.ReflectEnum.main(ReflectEnum.java:57) 

如果我们尝试按名称查找枚举常量

 System.out.println(Color.valueOf("GREEN")); System.out.println(Color.valueOf("RED")); System.out.println(Color.valueOf("BLUE")); 

我们看到最后一次改变突破了更多:

 Exception in thread "main" java.lang.NullPointerException at java.lang.Class.enumConstantDirectory(Class.java:3236) at java.lang.Enum.valueOf(Enum.java:232) at playground.Color.valueOf(Color.java:1) at playground.ReflectEnum.main(ReflectEnum.java:48) 

ReflectEnum.java:48行包含上面的Color.valueOf("GREEN") print语句。 此枚举常量未设置为null 。 所以它彻底打破了valueOf方法。

Enum.valueOf(Color.class, "BLUE")仍然解析枚举常量Color.BLUE

由于enum数组被声明为static final我使用Javareflection更改了private private final字段来创建和设置新的枚举数组。

  protected static > void setEnumsArray(Class ec, E... e) throws Exception { Field field = ec.getDeclaredField("ENUM$VALUES"); Field modifiersField = Field.class.getDeclaredField("modifiers"); field.setAccessible(true); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(ec, e); } 

但是执行setEnumsArray(Color.class, Color.BLUE, Color.GREEN, Color.RED, Color.BLUE)失败了

 Exception in thread "main" java.lang.IllegalAccessException: Can not set static final [Lplayground.Color; field playground.Color.ENUM$VALUES to [Lplayground.Color; at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76) at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80) at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77) at java.lang.reflect.Field.set(Field.java:758) at playground.ReflectEnum.setEnumsArray(ReflectEnum.java:76) at playground.ReflectEnum.main(ReflectEnum.java:37) 

编辑1(在Radiodef的评论之后 ):

之后我添加了以下两种方法……

  protected static Field getEnumsArrayField(Class ec) throws Exception { Field field = ec.getDeclaredField("ENUM$VALUES"); field.setAccessible(true); return field; } protected static void clearFieldAccessors(Field field) throws ReflectiveOperationException { Field fa = Field.class.getDeclaredField("fieldAccessor"); fa.setAccessible(true); fa.set(field, null); Field ofa = Field.class.getDeclaredField("overrideFieldAccessor"); ofa.setAccessible(true); ofa.set(field, null); Field rf = Field.class.getDeclaredField("root"); rf.setAccessible(true); Field root = (Field) rf.get(field); if (root != null) { clearFieldAccessors(root); } 

我试过这个:

 System.out.println(Arrays.toString((Object[]) getEnumsArrayField(Color.class).get(null))); clearFieldAccessors(getEnumsArrayField(Color.class)); setEnumsArray(Color.class, Color.BLUE, Color.GREEN, Color.RED, Color.BLUE); System.out.println(Arrays.toString(Color.values())); 

由此可见:

 [RED, GREEN, BLUE] [BLUE, GREEN, RED, BLUE] 

枚举数组已被另一个替换。


编辑2(在GotoFinal的评论之后 )
根据GotoFinal关于如何在java中使用reflection创建枚举实例的答案 ? 可以在运行时创建更多枚举实例。 然后应该可以用另一个实例替换枚举单例实例。 我有以下枚举单例:

  public enum Singleton { INSTANCE("The one and only"); private String description; private Singleton(String description) { this.description = description; } @Override public String toString() { return description; } } 

重用代码GotoFinal已经显示我定义了以下方法:

  protected static Singleton createEnumValue(String name, int ordinal, String description) throws Exception { Class monsterClass = Singleton.class; Constructor constructor = monsterClass.getDeclaredConstructors()[0]; constructor.setAccessible(true); Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor"); constructorAccessorField.setAccessible(true); sun.reflect.ConstructorAccessor ca = (sun.reflect.ConstructorAccessor) constructorAccessorField.get(constructor); if (ca == null) { Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor"); acquireConstructorAccessorMethod.setAccessible(true); ca = (sun.reflect.ConstructorAccessor) acquireConstructorAccessorMethod.invoke(constructor); } Singleton enumValue = (Singleton) ca.newInstance(new Object[] { name, ordinal, description }); return enumValue; } protected static > void setFinalField(Class ec, Field field, E e) throws NoSuchFieldException, IllegalAccessException { Field modifiersField = Field.class.getDeclaredField("modifiers"); field.setAccessible(true); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(ec, e); } 

正在执行

 System.out.println(Singleton.INSTANCE.toString()); // setting INSTANCE = theNewOne Singleton theNewOne = createEnumValue(Singleton.INSTANCE.name(), Singleton.INSTANCE.ordinal(), "The new one!"); setFinalField(Singleton.class, Singleton.class.getDeclaredField(Singleton.INSTANCE.name()), theNewOne); System.out.println(Singleton.INSTANCE.toString()); // setting enum array = [theNewOne] clearFieldAccessors(getEnumsArrayField(Singleton.class)); setEnumsArray(Singleton.class, theNewOne); System.out.println(Arrays.toString(Singleton.values())); 

说明:

 The one and only The new one! [The new one!] 

总结:

  • 可以在运行时修改枚举并用另一个枚举数组替换枚举数组。 但至少将枚举常量设置为null会破坏VM中已定义枚举的一致性。 虽然这可以根据GotoFinal的答案修复。

  • 当使用enum实现单例时,可以用另一个枚举实例替换唯一的一个实例 – 至少对于知道它们的实现的一些JDK / JRE。 如果枚举构造函数具有参数,则新创建的枚举实例可以利用它们来植入恶​​意行为。

是的,您甚至可以在没有任何问题的情况下为枚举添加新值,正如我在此处所述: https : //stackoverflow.com/a/51244909/4378853

所以你有多种方法来打破枚举:
1.使用reflection将Color.RED等字段的值更改为其他枚举值。
2.使用reflection通过修改ENUM$VALUES字段来更改.values。
3.使用reflection通过编辑Class类中的T[] enumConstants来更改MyEnum.class.getEnumConstants()返回的内容。
4.类似于Class类中的Map enumConstantDirectory – 用于Enum.valueOf(MyEnum.class, name)MyEnum.valueOf(name)
5.使用reflection添加新的枚举常量,如上面的链接答案中所述。

因此对于

关于以下枚举,是否可以添加颜色WHITE或删除颜色RED或更改其顺序?

对的,这是可能的。

 Class enumClass = Color.class; // first we need to find our constructor, and make it accessible Constructor constructor = enumClass.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 -> iternal 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 Color enumValue = (Color) ca.newInstance(new Object[]{"WHITE", 3});// you can call that using reflections too, reflecting reflections are best part of java ;) 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 = Color.class.getDeclaredField("$VALUES"); makeAccessible(valuesField); // just copy old values to new array and add our new field. Color[] oldValues = (Color[]) valuesField.get(null); Color[] newValues = new Color[oldValues.length + 1]; System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); newValues[oldValues.length] = enumValue; valuesField.set(null, newValues); 

现在还使用reflection来更新Class类中的两个字段。

 Field enumConstantDirectoryField = Class.class.getDeclaredField("enumConstantDirectory"); enumConstantDirectoryField.setAccessible(true); enumConstantDirectoryField.set(Color.class, null); Field enumConstantsField = Class.class.getDeclaredField("enumConstants"); enumConstantsField.setAccessible(true); enumConstantsField.set(Color.class, null); 

要更改订单,只需使用与$VALUES字段编辑相同的代码,但只需将其设置为您想要的任何内容即可。

您不仅可以添加/删除make Color.WHITE等字段,也可以使Color.RED消失(但您可以将其设置为null)

同样使用unsafe,应该可以声明新类来创建抽象枚举类的新实例。
我使用javassist库来减少生成新类所需的代码:

 public class Test { public static void main(String[] args) throws Exception { System.out.println(MyEnum.VALUE.getSomething()); // prints 5 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()); // prints 3 } 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.注意,这是不可能枚举不包含子类的类 – 所以没有任何重写方法,因为枚举被声明为最终类。

并且您可以使用它来打破单例模式而不会出现任何问题,只需将其设置为null,或者您甚至可以尝试使用unsafe在运行时实现自己的版本并替换它的实例,或者只创建它的多个实例。
这就是为什么我更喜欢只有私有构造函数和持有者类的简单类 – 如果有人想打破它,他们无论如何都会这样做。 但至少你不要将enum用于它不适合的东西,因此代码更容易阅读。