有人可以解释为什么以下代码失败了吗?我有以下五个班级:
public class TestReplaceLogger {
public static void main(String[] arv) throws Exception {
ClassWithFinalFields classWithFinalFields = new ClassWithFinalFields();
Field field = ClassWithFinalFields.class.getDeclaredField("LOG");
// Comment this line and uncomment out the next line causes program work
Logger oldLogger = (Logger)field.get(null);
//Logger oldLogger = classWithFinalFields.LOG;
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, new MockLogger());
classWithFinalFields.log();
}
}
public class ClassWithFinalFields {
public static final Logger LOG = new RealLogger();
public void log() {
LOG.log("hello");
}
}
public interface Logger {
public void log(String msg);
}
public class RealLogger implements Logger{
public void log(String msg) {
System.out.println("RealLogger: " + msg);
}
}
public class MockLogger implements Logger {
public void log(String msg) {
System.out.println("MockLogger: " + msg);
}
}
代码尝试做的是使用反射来替换ClassWithFinalFields类中的LOG变量。就目前而言,当该类尝试在IllegalAccessException
末尾设置字段时,该类会抛出TestReplaceLogger
。
但是,如果我更换
Logger oldLogger = (Logger)field.get(null);
与
Logger oldLogger = classWithFinalFields.LOG;
然后代码运行没有问题,并按预期打印日志“MockLogger:hello”。
所以问题是,为什么通过反射阅读最终字段会使程序停止工作?看起来无法再删除最终修饰符,因此您获得了IllegalAccessException,但我不知道为什么。我可以推测这可能与编译器优化或类加载器排序有关,但是,尽管看过字节代码,我还不知道发生了什么。
如果人们想知道我为什么要这样做,那么它就开始寻找一种方法来模拟在单元测试期间的一些笨拙的日志记录,同时我们正在升级一些软件。现在我只是好奇地想知道幕后发生了什么。
如果有人想看到它,堆栈跟踪是
Exception in thread "main" java.lang.IllegalAccessException: Can not set static final org.matthelliwell.reflection.Logger field org.matthelliwell.reflection.ClassWithFinalFields.LOG to org.matthelliwell.reflection.MockLogger
at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:73)
at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:77)
at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
at java.lang.reflect.Field.set(Field.java:741)
at org.matthelliwell.reflection.TestReplaceLogger.main(TestReplaceLogger.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
答案 0 :(得分:5)
您正在绕过其公共API访问字段对象。当然,如果你这样做,任何事情都可能发生。特别是,Java API的不同实现可能表现不同。
在Oracle JDK中,Field假定修饰符为final,因此缓存fieldAccessor(请参阅Field.getFieldAccessor()
)。您已经更改了修饰符,但没有使该缓存失效,从而导致使用旧的字段访问器,它仍然认为该字段是最终的。
答案 1 :(得分:3)
移动这些行:
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
之前:
Logger oldLogger = (Logger)field.get(null);
答案 2 :(得分:1)
有一个类似的问题,它几乎让我脱掉了自己的头发。
这段代码(简化)可以完美地运作......
Field field = Long.class.getDeclaredField("MIN_VALUE");
field.get(null);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.get(null);
...这个会抛出“IllegalAccessException”
Field field = Long.class.getDeclaredField("MIN_VALUE");
field.get(null);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, 1000l);
(由于上述原因)
答案 3 :(得分:0)
字段的修饰符必须在字段上的任何操作之前更改,否则JVM会在第一次操作时缓存字段元数据(如@meriton和@ oleg.lukyrych所述)