Ändern Sie das private statische Endfeld mithilfe der Java-Reflektion


479

Ich habe eine Klasse mit einem private static finalFeld, das ich leider zur Laufzeit ändern muss.

Mit Reflection erhalte ich folgenden Fehler: java.lang.IllegalAccessException: Can not set static final boolean field

Gibt es eine Möglichkeit, den Wert zu ändern?

Field hack = WarpTransform2D.class.getDeclaredField("USE_HACK");
hack.setAccessible(true);
hack.set(null, true);

4
So eine schlechte Idee. Ich würde versuchen, die Quelle abzurufen und stattdessen neu zu kompilieren (oder sogar zu dekompilieren / neu zu kompilieren).
Bill K

System.out ist ein öffentliches statisches Endfeld, kann aber auch geändert werden.
Unbestreitbarer

19
@irreputable System.out/in/errsind so "speziell", dass das Java-Speichermodell sie besonders erwähnen muss. Sie sind keine Beispiele, die befolgt werden sollten.
Tom Hawtin - Tackline

8
Nun, mein Punkt ist es, einen Hack dazwischen zu finden, damit meine App funktioniert, bis die verantwortliche Bibliothek die Änderung bei der nächsten Version
vornimmt,

1
@ Bill K von vor zehn Jahren: Es wäre großartig, es neu zu kompilieren, aber es befindet sich auf einem bereitgestellten System und ich muss es nur patchen, bis wir die bereitgestellte App aktualisieren können!
Bill K

Antworten:


888

Keine Angenommen SecurityManagerwird Sie tun dies zu verhindern, können Sie setAccessibleerhalten um privateden Modifikator und Zurücksetzen , um loszuwerden final, und tatsächlich ein ändern private static finalFeld.

Hier ist ein Beispiel:

import java.lang.reflect.*;

public class EverythingIsTrue {
   static void setFinalStatic(Field field, Object newValue) throws Exception {
      field.setAccessible(true);

      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

      field.set(null, newValue);
   }
   public static void main(String args[]) throws Exception {      
      setFinalStatic(Boolean.class.getField("FALSE"), true);

      System.out.format("Everything is %s", false); // "Everything is true"
   }
}

Angenommen, nein SecurityExceptionwird geworfen, wird der obige Code gedruckt "Everything is true".

Was hier tatsächlich gemacht wird, ist wie folgt:

  • Die primitiven booleanWerte trueund falsein mainwerden automatisch an den Referenztyp Boolean"Konstanten" Boolean.TRUEund " referenziert "Boolean.FALSE
  • Reflexion wird verwendet, um das zu ändern public static final Boolean.FALSE, um auf das BooleanBezug zu nehmen, auf das verwiesen wirdBoolean.TRUE
  • Infolgedessen bezieht sich a, wann immer ein falseAutobox gesendet wird Boolean.FALSE, auf dasselbe Booleanwie das, auf das von verwiesen wirdBoolean.TRUE
  • Alles was "false"jetzt war ist"true"

Verwandte Fragen


Vorsichtsmaßnahmen

Wenn Sie so etwas tun, sollten Sie äußerst vorsichtig sein. Es funktioniert möglicherweise nicht, weil a SecurityManagervorhanden sein kann, aber selbst wenn dies nicht der Fall ist, kann es je nach Verwendungsmuster funktionieren oder auch nicht.

JLS 17.5.3 Nachträgliche Änderung der endgültigen Felder

In einigen Fällen, z. B. bei der Deserialisierung, muss das System die finalFelder eines Objekts nach der Erstellung ändern . finalFelder können durch Reflexion und andere implementierungsabhängige Mittel geändert werden. Das einzige Muster, in dem dies eine vernünftige Semantik hat, ist eines, in dem ein Objekt konstruiert wird und dann die finalFelder des Objekts aktualisiert werden. Das Objekt sollte weder für andere Threads sichtbar gemacht noch die finalFelder gelesen werden, bis alle Aktualisierungen der finalFelder des Objekts abgeschlossen sind. Einfrieren eines finalFeldes tritt sowohl am Ende des Konstruktors, in dem das finalFeld eingestellt ist, als auch unmittelbar nach jeder Änderung eines finalFeldes durch Reflexion oder einen anderen speziellen Mechanismus auf.

Selbst dann gibt es eine Reihe von Komplikationen. Wenn ein finalFeld in der Felddeklaration mit einer Kompilierungszeitkonstante initialisiert wird, werden Änderungen am finalFeld möglicherweise nicht beobachtet, da die Verwendung dieses finalFelds zur Kompilierungszeit durch die Kompilierungszeitkonstante ersetzt wird.

Ein weiteres Problem besteht darin, dass die Spezifikation eine aggressive Optimierung von finalFeldern ermöglicht. Innerhalb eines Threads ist es zulässig, Lesevorgänge eines finalFeldes mit den Änderungen eines endgültigen Felds neu anzuordnen , die nicht im Konstruktor stattfinden.

Siehe auch

  • JLS 15.28 Konstante Expression
    • Es ist unwahrscheinlich, dass diese Technik mit einem private static final booleanGrundelement funktioniert , da sie als Konstante für die Kompilierungszeit inlinierbar ist und daher der "neue" Wert möglicherweise nicht beobachtbar ist

Anhang: Zur bitweisen Manipulation

Im Wesentlichen,

field.getModifiers() & ~Modifier.FINAL

schaltet das Bit aus von Modifier.FINALaus field.getModifiers(). &ist das bitweise und und ~ist das bitweise Komplement.

Siehe auch


Denken Sie an konstante Ausdrücke

Immer noch nicht in der Lage, dies zu lösen? Sind Sie auf Depressionen gefallen, wie ich es dafür getan habe? Sieht Ihr Code so aus?

public class A {
    private final String myVar = "Some Value";
}

Beim Lesen der Kommentare zu dieser Antwort, insbesondere der von @Pshemo, wurde ich daran erinnert, dass konstante Ausdrücke unterschiedlich behandelt werden, sodass es unmöglich ist , sie zu ändern. Daher müssen Sie Ihren Code so ändern, dass er so aussieht:

public class A {
    private final String myVar;

    private A() {
        myVar = "Some Value";
    }
}

wenn du nicht der Besitzer der Klasse bist ... ich fühle dich!

Für weitere Details darüber, warum dieses Verhalten dies liest ?


41
@thecoop, @HalfBrian: Es besteht kein Zweifel, dass dies EXTREM BÖSE ist , aber dieses Beispiel wurde mit Absicht ausgewählt. Meine Antwort zeigt nur, wie dies unter Umständen möglich ist. Das ekelhafteste Beispiel, an das ich denken kann, ist die bewusste Wahl in der Hoffnung, dass die Leute vielleicht sofort angewidert sind, anstatt sich in die Technik zu verlieben.
Polygenelubricants

59
Yo Kumpel. Ich habe gehört, dass du Reflexion magst, also habe ich über das Feld nachgedacht, damit du reflektieren kannst, während du reflektierst.
Matthew Flaschen

11
Beachten Sie, dass Boolean.FALSE nicht privat ist. Funktioniert dies wirklich mit "privaten endgültigen statischen" Mitgliedern?
mgaert

15
@mgaert tut es, aber Sie müssen getDeclaredField()anstelle getField()für Zielklasse verwenden
Eis

11
+1. Für diejenigen, die versuchen, etwas wie zu ändern, final String myConstant = "x";und fehlschlagen: Denken Sie daran, dass Konstanten für die Kompilierungszeit vom Compiler eingefügt werden. Wenn Sie also Code schreiben System.out.println(myConstant);, wird dieser kompiliert, System.out.println("x");da der Compiler den Wert der Konstanten zur Kompilierungszeit kennt. Um dieses Problem zu beheben, müssen Sie Ihre Konstanten zur Laufzeit wie initialisieren final String myConstant = new String("x");. Auch bei final int myField = 11final int myField = new Integer(11);final Integer myField = 11;
Grundelementen

58

Wenn der einem static final booleanFeld zugewiesene Wert zur Kompilierungszeit bekannt ist, ist er eine Konstante. Felder des Grundelements oder des StringTyps können Konstanten zur Kompilierungszeit sein. In jedem Code, der auf das Feld verweist, wird eine Konstante eingefügt. Da das Feld zur Laufzeit nicht gelesen wird, hat eine Änderung keine Auswirkung.

Die Java-Sprachspezifikation sagt dies:

Wenn ein Feld eine konstante Variable ist (§4.12.4), wird durch das Löschen des Schlüsselworts final oder das Ändern seines Werts die Kompatibilität mit bereits vorhandenen Binärdateien nicht beeinträchtigt, da diese nicht ausgeführt werden. Es wird jedoch kein neuer Wert für die Verwendung angezeigt des Feldes, es sei denn, sie werden neu kompiliert. Dies gilt auch dann, wenn die Verwendung selbst kein konstanter Ausdruck zur Kompilierungszeit ist (§15.28).

Hier ist ein Beispiel:

class Flag {
  static final boolean FLAG = true;
}

class Checker {
  public static void main(String... argv) {
    System.out.println(Flag.FLAG);
  }
}

Wenn Sie dekompilieren Checker, werden Sie feststellen , dass Flag.FLAGder Code anstelle einer Referenzierung einfach den Wert 1 ( true) auf den Stapel schiebt (Anweisung 3).

0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
3:   iconst_1
4:   invokevirtual   #3; //Method java/io/PrintStream.println:(Z)V
7:   return

Das war mein erster Gedanke, aber dann erinnerte ich mich an Java, das zur Laufzeit kompiliert wurde. Wenn Sie das Bit zurücksetzen würden, würde es einfach als Variable statt als Konstante neu kompiliert.
Bill K

4
@ Bill K - Nein, dies bezieht sich nicht auf die JIT-Kompilierung. Die abhängigen Klassendateien enthalten tatsächlich die Inline-Werte und keinen Verweis auf die unabhängige Klasse. Es ist ein ziemlich einfaches Experiment zum Testen; Ich werde ein Beispiel hinzufügen.
Erickson

1
Wie passt das zu der Antwort von @polygenelubricants, in der er Boolean.false neu definiert? - aber Sie haben Recht, ich habe dieses Verhalten gesehen, als die Dinge nicht richtig neu kompiliert wurden.
Bill K

26
@ Bill K - In der Antwort von Polygenlubricants ist das Feld keine Kompilierungszeitkonstante. Es ist public static final Boolean FALSE = new Boolean(false)nichtpublic static final boolean FALSE = false
erickson

17

Eine kleine Kuriosität aus der Java-Sprachspezifikation, Kapitel 17, Abschnitt 17.5.4 "Schreibgeschützte Felder":

Normalerweise darf ein Feld, das endgültig und statisch ist, nicht geändert werden. System.in, System.out und System.err sind jedoch statische Endfelder, die aus älteren Gründen durch die Methoden System.setIn, System.setOut und System.setErr geändert werden dürfen. Wir bezeichnen diese Felder als schreibgeschützt, um sie von normalen Endfeldern zu unterscheiden.

Quelle: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.4


9

Ich habe es auch in die Joor-Bibliothek integriert

Benutz einfach

      Reflect.on(yourObject).set("finalFieldName", finalFieldValue);

Außerdem habe ich ein Problem behoben, bei overridedem die vorherigen Lösungen zu fehlen scheinen. Gehen Sie jedoch nur sehr vorsichtig damit um, wenn es keine andere gute Lösung gibt.


Wenn ich dies versuche (JDK12), erhalte ich eine Ausnahme: "Endgültiges ___ Feld kann nicht gesetzt werden".
Aaron Iba

@AaronIba In Java 12+ ist dies nicht mehr zulässig.
NateS

7

Zusammen mit der am besten bewerteten Antwort können Sie einen etwas einfachsten Ansatz verwenden. Die Apache Commons- FieldUtilsKlasse verfügt bereits über eine bestimmte Methode, mit der das erledigt werden kann. Bitte werfen Sie einen Blick auf die FieldUtils.removeFinalModifierMethode. Sie sollten die Zielfeldinstanz und das Barrierefreiheits-Forcing-Flag angeben (wenn Sie mit nicht öffentlichen Feldern spielen). Weitere Infos finden Sie hier .


Dies ist eine viel einfachere Lösung als die derzeit akzeptierte Antwort
Bernie

4
Ist es? Das Kopieren einer Methode klingt nach einer einfacheren Lösung als das Importieren einer gesamten Bibliothek (was dasselbe tut wie die Methode, die Sie kopieren würden).
Eski

1
Funktioniert nicht in Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR

6

Bei Vorhandensein eines Sicherheitsmanagers kann davon Gebrauch gemacht werden AccessController.doPrivileged

Nehmen Sie das gleiche Beispiel aus der oben akzeptierten Antwort:

import java.lang.reflect.*;

public class EverythingIsTrue {
    static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");

        // wrapping setAccessible 
        AccessController.doPrivileged(new PrivilegedAction() {
            @Override
            public Object run() {
                modifiersField.setAccessible(true);
                return null;
            }
        });

        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue);
    }

    public static void main(String args[]) throws Exception {      
      setFinalStatic(Boolean.class.getField("FALSE"), true);
      System.out.format("Everything is %s", false); // "Everything is true"
    }
}

Im Lambda-Ausdruck AccessController.doPrivilegedkann vereinfacht werden zu:

AccessController.doPrivileged((PrivilegedAction) () -> {
    modifiersField.setAccessible(true);
    return null;
});

1
Dies scheint auch mit Java 12+ nicht zu funktionieren.
Dan1st

ja @ dan1st, du hast recht! Bitte überprüfen Sie dies für eine Lösung: stackoverflow.com/a/56043252/2546381
VanagaS

2

Die akzeptierte Antwort funktionierte für mich bis zur Bereitstellung auf JDK 1.8u91. Dann wurde mir klar, dass es in der field.set(null, newValue);Zeile fehlgeschlagen war , als ich den Wert vor dem Aufruf von durch Reflektion gelesen hattesetFinalStatic Methode .

Wahrscheinlich hat das Lesen irgendwie zu einem anderen Setup von Java Reflection-Interna geführt (nämlich sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImplim Fehlerfall stattsun.reflect.UnsafeStaticObjectFieldAccessorImpl im Erfolgsfall), aber ich habe es nicht weiter ausgeführt.

Da ich vorübergehend einen neuen Wert basierend auf dem alten Wert und später einen alten Wert zurücksetzen musste, habe ich die Signatur ein wenig geändert, um die Berechnungsfunktion extern bereitzustellen und auch den alten Wert zurückzugeben:

public static <T> T assignFinalField(Object object, Class<?> clazz, String fieldName, UnaryOperator<T> newValueFunction) {
    Field f = null, ff = null;
    try {
        f = clazz.getDeclaredField(fieldName);
        final int oldM = f.getModifiers();
        final int newM = oldM & ~Modifier.FINAL;
        ff = Field.class.getDeclaredField("modifiers");
        ff.setAccessible(true);
        ff.setInt(f,newM);
        f.setAccessible(true);

        T result = (T)f.get(object);
        T newValue = newValueFunction.apply(result);

        f.set(object,newValue);
        ff.setInt(f,oldM);

        return result;
    } ...

Für den allgemeinen Fall wäre dies jedoch nicht ausreichend.


2

Auch trotz des Seins final ein Feld außerhalb des statischen Initialisierers geändert werden kann und (zumindest JVM HotSpot) den Bytecode einwandfrei ausführt.

Das Problem ist, dass der Java-Compiler dies nicht zulässt, dies kann jedoch leicht umgangen werden objectweb.asm. Hier ist eine perfekt gültige Klassendatei, die die Bytecode-Überprüfung besteht und erfolgreich unter JVM HotSpot OpenJDK12 geladen und initialisiert wurde:

ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Cl", null, "java/lang/Object", null);
{
    FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "fld", "I", null, null);
    fv.visitEnd();
}
{
    // public void setFinalField1() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField1", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_5);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
{
    // public void setFinalField2() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField2", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_2);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
cw.visitEnd();

In Java sieht die Klasse ungefähr so ​​aus:

public class Cl{
    private static final int fld;

    public static void setFinalField1(){
        fld = 5;
    }

    public static void setFinalField2(){
        fld = 2;
    }
}

mit denen nicht kompiliert werden kann javac , aber von JVM geladen und ausgeführt werden können.

JVM HotSpot behandelt solche Klassen speziell in dem Sinne, dass solche "Konstanten" nicht an einer konstanten Faltung teilnehmen können. Diese Überprüfung erfolgt in der Phase des Umschreibens des Bytecodes der Klasseninitialisierung :

// Check if any final field of the class given as parameter is modified
// outside of initializer methods of the class. Fields that are modified
// are marked with a flag. For marked fields, the compilers do not perform
// constant folding (as the field can be changed after initialization).
//
// The check is performed after verification and only if verification has
// succeeded. Therefore, the class is guaranteed to be well-formed.
InstanceKlass* klass = method->method_holder();
u2 bc_index = Bytes::get_Java_u2(bcp + prefix_length + 1);
constantPoolHandle cp(method->constants());
Symbol* ref_class_name = cp->klass_name_at(cp->klass_ref_index_at(bc_index));
if (klass->name() == ref_class_name) {
   Symbol* field_name = cp->name_ref_at(bc_index);
   Symbol* field_sig = cp->signature_ref_at(bc_index);

   fieldDescriptor fd;
   if (klass->find_field(field_name, field_sig, &fd) != NULL) {
      if (fd.access_flags().is_final()) {
         if (fd.access_flags().is_static()) {
            if (!method->is_static_initializer()) {
               fd.set_has_initialized_final_update(true);
            }
          } else {
            if (!method->is_object_initializer()) {
              fd.set_has_initialized_final_update(true);
            }
          }
        }
      }
    }
}

Die einzige Einschränkung, die JVM HotSpot überprüft, besteht darin, dass das finalFeld nicht außerhalb der Klasse geändert werden darf, in der das finalFeld deklariert ist.


0

Ich habe diese Frage gerade in einer der Interviewfragen gesehen, wenn möglich, um die endgültige Variable mit Reflexion oder zur Laufzeit zu ändern. Wurde wirklich interessiert, so dass, was ich wurde mit:

 /**
 * @author Dmitrijs Lobanovskis
 * @since 03/03/2016.
 */
public class SomeClass {

    private final String str;

    SomeClass(){
        this.str = "This is the string that never changes!";
    }

    public String getStr() {
        return str;
    }

    @Override
    public String toString() {
        return "Class name: " + getClass() + " Value: " + getStr();
    }
}

Eine einfache Klasse mit endgültiger String-Variable. Importieren Sie also in der Hauptklasse java.lang.reflect.Field;

/**
 * @author Dmitrijs Lobanovskis
 * @since 03/03/2016.
 */
public class Main {


    public static void main(String[] args) throws Exception{

        SomeClass someClass = new SomeClass();
        System.out.println(someClass);

        Field field = someClass.getClass().getDeclaredField("str");
        field.setAccessible(true);

        field.set(someClass, "There you are");

        System.out.println(someClass);
    }
}

Die Ausgabe wird wie folgt sein:

Class name: class SomeClass Value: This is the string that never changes!
Class name: class SomeClass Value: There you are

Process finished with exit code 0

Laut Dokumentation https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html


Hast du diesen Beitrag gesehen?
Ravindra HV

Diese Frage fragt nach einem staticletzten Feld, daher funktioniert dieser Code nicht. setAccessible(true)funktioniert nur zum Festlegen von Endinstanzfeldern.
Radiodef

0

Wenn Ihr Feld einfach privat ist, können Sie dies tun:

MyClass myClass= new MyClass();
Field aField= myClass.getClass().getDeclaredField("someField");
aField.setAccessible(true);
aField.set(myClass, "newValueForAString");

und werfen / behandeln NoSuchFieldException


-4

Der springende Punkt eines final Feldes ist, dass es nach dem Setzen nicht mehr neu zugewiesen werden kann. Die JVM verwendet diese Garantie, um die Konsistenz an verschiedenen Stellen aufrechtzuerhalten (z. B. innere Klassen, die auf äußere Variablen verweisen). Also nein. In der Lage zu sein, würde die JVM brechen!

Die Lösung besteht nicht darin, es überhaupt zu deklarieren final.


4
Darüber hinaus finalspielt die Multithread-Ausführung eine besondere Rolle - das Ändern von finalWerten würde auch das Java-Speichermodell beschädigen.
Péter Török

1
Und ein nicht deklariertes Feld finalsollte nicht deklariert werden static.
Tom Hawtin - Tackline

2
@ Tom: Im Allgemeinen ist das wahrscheinlich wahr, aber ich würde nicht alle statischen veränderlichen Variablen verbieten .
Bcat

7
@ Tom: Hast du jemals gelesen, warum Singletons böse sind? Ich tat! Jetzt weiß ich, dass sie nur in Java böse sind. Und das nur wegen der Verfügbarkeit eines benutzerdefinierten Klassenladeprogramms. Und seit ich das alles weiß und keinen benutzerdefinierten Klassenlader verwende, verwende ich Singletons ohne Reue. Und Scala auch, wo Singleton ein erstklassiges Sprachmerkmal ist - Dass Singletons böse sind, ist ein bekannter falscher Mythos .
Martin

3
@Martin Ich weiß, dass Ihr Kommentar alt ist und sich Ihre Ansichten vielleicht inzwischen geändert haben, aber ich dachte, ich würde nur Folgendes hinzufügen: Singletons sind aus Gründen böse, die nichts mit Java zu tun haben. Sie fügen Ihrem Code versteckte Komplexität hinzu . Darüber hinaus können sie einen Unit-Test unmöglich machen, ohne zu wissen, dass n Singletons zuerst konfiguriert werden müssen. Sie sind das Gegenteil von Abhängigkeitsinjektion. Ihr Team könnte die Entscheidung treffen, dass die Fallstricke einer verborgenen Komplexität die Bequemlichkeit von Singletons nicht überwiegen, aber viele Teams vertreten aus gutem Grund die gegenteilige Haltung.
Crush
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.