Modifier le champ final statique privé à l'aide de la réflexion Java


479

J'ai une classe avec un private static finalchamp que, malheureusement, je dois le changer au moment de l'exécution.

En utilisant la réflexion, j'obtiens cette erreur: java.lang.IllegalAccessException: Can not set static final boolean field

Existe-t-il un moyen de modifier la valeur?

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

4
Une si mauvaise idée. J'essaierais d'obtenir la source et de recompiler (ou même de décompiler / recompiler) à la place.
Bill K

System.out est un champ final statique public, mais il peut également être modifié.
irréputable

19
@irreputable System.out/in/errsont si "spéciaux" que le modèle de mémoire Java doit en faire une mention spéciale. Ce ne sont pas des exemples à suivre.
Tom Hawtin - tackline

8
Eh

1
@Bill K d'il y a dix ans: Ce serait bien de le recompiler mais c'est sur un système déployé et j'ai juste besoin de le patcher jusqu'à ce que nous puissions mettre à jour l'application déployée!
Bill K

Réponses:


888

En supposant que non SecurityManagervous empêche de le faire, vous pouvez utiliser setAccessiblepour vous déplacer privateet réinitialiser le modificateur pour vous en débarrasser final, et modifier réellement un private static finalchamp.

Voici un exemple:

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"
   }
}

En supposant que non SecurityExceptionest levé, le code ci-dessus s'imprime "Everything is true".

Ce qui est réellement fait ici est le suivant:

  • Les booleanvaleurs primitives trueet falsein mainsont automatiquement encadrées pour faire référence aux Booleanconstantes de type Boolean.TRUEetBoolean.FALSE
  • La réflexion est utilisée pour changer la public static final Boolean.FALSEfaire référence à la Booleancité parBoolean.TRUE
  • En conséquence, par la suite chaque fois qu'un falseest autoboxed à Boolean.FALSE, elle fait référence à la même Booleanque celle référée parBoolean.TRUE
  • Tout ce qui était "false"maintenant est"true"

Questions connexes


Avertissements

Un soin extrême doit être pris chaque fois que vous faites quelque chose comme ça. Cela peut ne pas fonctionner car un SecurityManagerpeut être présent, mais même si ce n'est pas le cas, selon le modèle d'utilisation, cela peut ou peut ne pas fonctionner.

JLS 17.5.3 Modification ultérieure des champs finaux

Dans certains cas, comme la désérialisation, le système devra modifier les finalchamps d'un objet après la construction. finalles champs peuvent être modifiés via la réflexion et d'autres moyens dépendant de la mise en œuvre. Le seul modèle dans lequel cela a une sémantique raisonnable est celui dans lequel un objet est construit et ensuite les finalchamps de l'objet sont mis à jour. L'objet ne doit pas être rendu visible aux autres threads, et les finalchamps ne doivent pas être lus jusqu'à ce que toutes les mises à jour des finalchamps de l'objet soient terminées. Les blocages d'un finalchamp se produisent à la fois à la fin du constructeur dans lequel le finalchamp est défini et immédiatement après chaque modification d'un finalchamp via la réflexion ou un autre mécanisme spécial.

Même alors, il y a un certain nombre de complications. Si un finalchamp est initialisé à une constante de temps de compilation dans la déclaration de champ, les modifications apportées au finalchamp peuvent ne pas être observées, car les utilisations de ce finalchamp sont remplacées au moment de la compilation par la constante de temps de compilation.

Un autre problème est que la spécification permet une optimisation agressive des finalchamps. Au sein d'un thread, il est possible de réorganiser les lectures d'un finalchamp avec les modifications d'un champ final qui n'ont pas lieu dans le constructeur.

Voir également

  • JLS 15.28 Expression constante
    • Il est peu probable que cette technique fonctionne avec une primitive private static final boolean, car elle est inlineable comme constante de temps de compilation et donc la "nouvelle" valeur peut ne pas être observable

Annexe: sur la manipulation au niveau du bit

Essentiellement,

field.getModifiers() & ~Modifier.FINAL

désactive le bit correspondant à Modifier.FINALfrom field.getModifiers(). &est le bit à bit et et ~est le complément au bit.

Voir également


N'oubliez pas les expressions constantes

Vous ne parvenez toujours pas à résoudre ce problème?, Êtes tombé dans la dépression comme je l'ai fait pour cela? Votre code ressemble à ceci?

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

En lisant les commentaires sur cette réponse, en particulier celle de @Pshemo, cela m'a rappelé que les expressions constantes sont traitées différemment, il sera donc impossible de la modifier. Par conséquent, vous devrez modifier votre code pour qu'il ressemble à ceci:

public class A {
    private final String myVar;

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

si vous n'êtes pas le propriétaire de la classe ... je vous sens!

Pour plus de détails sur la raison de ce comportement, lisez ceci ?


41
@thecoop, @HalfBrian: il ne fait aucun doute que c'est EXTRÊMEMENT MAL , mais cet exemple a été choisi par conception. Ma réponse montre seulement comment, dans certaines circonstances, cela est possible. L'exemple le plus dégoûtant auquel je puisse penser est délibérément choisi avec l'espoir que peut-être les gens seraient instantanément dégoûtés au lieu de tomber amoureux de la technique.
polygenelubricants

59
Yo, dawg. Je vous ai entendu aimer la réflexion, alors j'ai réfléchi sur le terrain pour que vous puissiez réfléchir pendant que vous réfléchissez.
Matthew Flaschen

11
Notez que Boolean.FALSE n'est pas privé. Cela fonctionne-t-il vraiment avec les membres "final final statique"?
mgaert

15
@mgaert c'est le cas, mais vous devez utiliser getDeclaredField()au lieu de getField()pour la classe cible
eis

11
+1. Pour ceux qui essaieront de changer quelque chose comme final String myConstant = "x";et échoueront: rappelez-vous que les constantes de temps de compilation seront alignées par le compilateur, donc quand vous écrirez du code comme System.out.println(myConstant);il sera compilé comme System.out.println("x");parce que le compilateur connaît la valeur de constante au moment de la compilation. Pour vous débarrasser de ce problème, vous devez initialiser vos constantes lors de l'exécution comme final String myConstant = new String("x");. Aussi en cas de primitives comme final int myField = 11utilisation final int myField = new Integer(11);oufinal Integer myField = 11;
Pshemo

58

Si la valeur affectée à un static final booleanchamp est connue au moment de la compilation, il s'agit d'une constante. Les champs de Stringtype primitif ou de type peuvent être des constantes au moment de la compilation. Une constante sera insérée dans tout code faisant référence au champ. Étant donné que le champ n'est pas réellement lu au moment de l'exécution, sa modification n'aura alors aucun effet.

La spécification du langage Java dit ceci:

Si un champ est une variable constante (§4.12.4), la suppression du mot-clé final ou la modification de sa valeur ne rompra pas la compatibilité avec les binaires préexistants en les empêchant de s'exécuter, mais ils ne verront aucune nouvelle valeur pour l'utilisation du champ à moins qu'ils ne soient recompilés. Cela est vrai même si l'utilisation elle-même n'est pas une expression constante de compilation (§15.28)

Voici un exemple:

class Flag {
  static final boolean FLAG = true;
}

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

Si vous décompilez Checker, vous verrez qu'au lieu de faire référence Flag.FLAG, le code pousse simplement une valeur de 1 ( true) sur la pile (instruction # 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

C'était ma première pensée, mais je me suis souvenu de Java compilé au moment de l'exécution, si vous réinitialisiez le bit, il se recompilerait simplement avec comme une variable au lieu d'une constante.
Bill K

4
@Bill K - Non, cela ne fait pas référence à la compilation JIT. Les fichiers de classe dépendants contiendront en fait les valeurs intégrées et aucune référence à la classe indépendante. C'est une expérience assez simple à tester; J'ajouterai un exemple.
erickson

1
Comment cela fonctionne-t-il avec la réponse de @polygenelubricants où il redéfinit Boolean.false? - mais vous avez raison, j'ai vu ce comportement lorsque les choses ne se sont pas recompilées correctement.
Bill K

26
@Bill K - dans la réponse des polygénlubrifiants, le champ n'est pas une constante de temps de compilation. Ce n'est public static final Boolean FALSE = new Boolean(false)paspublic static final boolean FALSE = false
erickson

17

Un peu de curiosité de la spécification du langage Java, chapitre 17, section 17.5.4 "Champs protégés en écriture":

Normalement, un champ final et statique ne peut pas être modifié. Cependant, System.in, System.out et System.err sont des champs finaux statiques qui, pour des raisons héritées, doivent être autorisés à être modifiés par les méthodes System.setIn, System.setOut et System.setErr. Nous nous référons à ces champs comme étant protégés en écriture pour les distinguer des champs finaux ordinaires.

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


9

Je l'ai également intégré à la bibliothèque Joor

Utilisez simplement

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

J'ai également corrigé un problème avec overridelequel les solutions précédentes semblent manquer. Cependant, utilisez-le très soigneusement, uniquement lorsqu'il n'y a pas d'autre bonne solution.


Lorsque j'essaie ceci (JDK12), j'obtiens une exception: "Impossible de définir le champ ___ final".
Aaron Iba

@AaronIba Ce n'est plus autorisé dans Java 12+.
NateS

7

Avec la réponse la mieux classée, vous pouvez utiliser une approche un peu plus simple. La FieldUtilsclasse Apache commons a déjà une méthode particulière qui peut faire les choses. Veuillez regarder la FieldUtils.removeFinalModifierméthode. Vous devez spécifier l'instance de champ cible et l'indicateur de forçage d'accessibilité (si vous jouez avec des champs non publics). Plus d'informations que vous pouvez trouver ici .


C'est une solution beaucoup plus simple que la réponse actuellement acceptée
Bernie

4
C'est ça? Copier une méthode semble être une solution plus simple que d'importer une bibliothèque entière (ce qui fait la même chose que la méthode que vous copiez).
eski

1
Ne fonctionne pas dans Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR

6

En cas de présence d'un Security Manager, on peut utiliser AccessController.doPrivileged

En prenant le même exemple de la réponse acceptée ci-dessus:

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"
    }
}

Dans l'expression lambda,, AccessController.doPrivilegedpeut être simplifié pour:

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

1
Cela ne semble pas non plus fonctionner avec java 12+.
dan1st

oui @ dan1st, vous avez raison! Veuillez vérifier ceci pour une solution: stackoverflow.com/a/56043252/2546381
VanagaS

2

La réponse acceptée a fonctionné pour moi jusqu'à son déploiement sur JDK 1.8u91. Ensuite, j'ai réalisé qu'il avait échoué à la field.set(null, newValue);ligne lorsque j'avais lu la valeur par réflexion avant d'appeler la setFinalStaticméthode.

La lecture a probablement causé une configuration différente des internes de réflexion Java (à savoir sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImplen cas d'échec plutôt sun.reflect.UnsafeStaticObjectFieldAccessorImplqu'en cas de réussite), mais je ne l'ai pas développé davantage.

Comme j'avais besoin de définir temporairement une nouvelle valeur en fonction de l'ancienne valeur et de la rétablir ultérieurement, j'ai légèrement modifié la signature pour fournir une fonction de calcul en externe et également retourner l'ancienne valeur:

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;
    } ...

Cependant, dans le cas général, cela ne serait pas suffisant.


2

Même en dépit d'être finalun champ peut être modifié en dehors de l'initialiseur statique et (au moins JVM HotSpot) exécutera parfaitement le bytecode.

Le problème est que le compilateur Java ne permet pas cela, mais cela peut être facilement contourné en utilisant objectweb.asm. Voici un fichier de classe parfaitement valide qui passe la vérification du bytecode et qui est correctement chargé et initialisé sous JVM HotSpot OpenJDK12:

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();

En Java, la classe ressemble grosso modo à ce qui suit:

public class Cl{
    private static final int fld;

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

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

qui ne peut pas être compilé avec javac, mais peut être chargé et exécuté par JVM.

JVM HotSpot a un traitement spécial pour ces classes dans le sens où il empêche ces "constantes" de participer au repliement constant. Cette vérification est effectuée lors de la phase de réécriture du bytecode de l'initialisation de la classe :

// 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);
            }
          }
        }
      }
    }
}

La seule restriction que JVM HotSpot vérifie est que le finalchamp ne doit pas être modifié en dehors de la classe à laquelle le finalchamp est déclaré.


0

Je viens de voir cette question sur l'une des questions de l'interview, si possible pour changer la variable finale avec réflexion ou en runtime. Je suis vraiment intéressé, donc ce que je suis devenu avec:

 /**
 * @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();
    }
}

Une classe simple avec une variable String finale. Ainsi, dans la classe principale, importez 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);
    }
}

La sortie sera la suivante:

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

Selon la documentation https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html


Avez-vous vu ce post?
Ravindra HV

Cette question concerne un staticchamp final, donc ce code ne fonctionne pas. setAccessible(true)ne fonctionne que pour définir les champs d'instance finale.
Radiodef

0

Si votre domaine est simplement privé, vous pouvez le faire:

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

et lancer / gérer NoSuchFieldException


-4

L'intérêt d'un finalchamp est qu'il ne peut pas être réaffecté une fois défini. La JVM utilise cette garantie pour maintenir la cohérence à divers endroits (par exemple, les classes internes référençant les variables externes). Donc non. Le faire pourrait briser la JVM!

La solution n'est pas de le déclarer finalen premier lieu.


4
De plus, finala un rôle spécial dans l'exécution multithread - la modification des finalvaleurs briserait également le modèle de mémoire Java.
Péter Török

1
Et un champ non déclaré finalne doit pas être déclaré static.
Tom Hawtin - tackline

2
@Tom: En général, c'est probablement vrai, mais je ne proscrirais pas toutes les variables statiques mutables.
bcat

7
@Tom: Avez-vous déjà lu pourquoi les singletons sont mauvais? J'ai fait! Maintenant, je sais qu'ils ne sont mauvais qu'en Java. Et uniquement en raison de la disponibilité d'un chargeur de classe défini par l'utilisateur. Et depuis que je sais tout cela et que je n'utilise pas de chargeur de classe défini par l'utilisateur, j'utilise des singletons sans regret. Et il en va de même pour Scala où le singleton est un langage de première classe - Que les singletons sont mauvais est un faux mythe bien connu .
Martin

3
@Martin Je sais que votre commentaire est ancien, et peut-être que votre point de vue a changé, mais je pensais simplement ajouter ceci: les singletons sont mauvais pour des raisons qui n'ont rien à voir avec Java. Ils ajoutent une complexité cachée à votre code. De plus, ils peuvent rendre impossible le test unitaire sans savoir également que n singletons doivent également être configurés en premier. Ils sont l'antithèse de l'injection de dépendance. Votre équipe pourrait prendre la décision que les pièges de la complexité cachée ne l'emportent pas sur la commodité des singletons, mais de nombreuses équipes adoptent la position opposée pour une bonne raison.
écraser
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.