Reputation: 2678
Could someone explain why the following code fails? I have the following five classes:
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);
}
}
What the code is trying to do is to use reflection to replace the LOG variable in the ClassWithFinalFields class. As it stands, the class throws an IllegalAccessException
when it tries to set the field at the end of TestReplaceLogger
.
However, if I replace
Logger oldLogger = (Logger)field.get(null);
with
Logger oldLogger = classWithFinalFields.LOG;
then the code runs without problems and prints log "MockLogger: hello" as expected.
So the question is, why does reading the final field through reflection stop the program working? It looks like the final modifier can no longer be removed so you get an IllegalAccessException but I have no idea why. I can speculate that may be it is something to do with the compiler optimisation or classloader ordering but, despite having had a look at the byte code, I've no real idea what is going on.
If people are wondering why I would want to do this at all, it started off as looking for a way to mock out some awkward logging during unit tests whilst we were upgrading some software. Now I'm just curious as to what on earth is going on under the covers.
If anyone want to see it, the stack trace is
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)
Upvotes: 5
Views: 2725
Reputation: 655
Had a similar problem and it nearly made me tear my own hair off.
Where this code (simplified) would work perfectly...
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);
...this one would throw an "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);
(for the reasons explained above)
Upvotes: 1
Reputation: 21
Modifiers of a field must be altered prior to any operation on the field otherwise he field metadata will be cached by the JVM upon the first operation (as mentioned by @meriton and @oleg.lukyrych).
Upvotes: 0
Reputation:
Just move these lines :
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
before this :
Logger oldLogger = (Logger)field.get(null);
Upvotes: 3
Reputation: 70564
You are accessing the field object bypassing its public api. Of course, anything may happen if you do that. In particular, different implementations of the Java API may behave differently.
In the Oracle JDK, Field assumes the modifiers to be final, and therefore caches the fieldAccessor (see Field.getFieldAccessor()
). You have changed the modifiers, but not invalidated that cache, causing the old field accessor to be used, which still believes the field to be final.
Upvotes: 5