Cambiar el campo final estático privado usando la reflexión Java

479

Tengo una clase con un private static finalcampo que, desafortunadamente, necesito cambiarlo en tiempo de ejecución.

Usando la reflexión me sale este error: java.lang.IllegalAccessException: Can not set static final boolean field

¿Hay alguna forma de cambiar el valor?

Field hack = WarpTransform2D.class.getDeclaredField("USE_HACK");
hack.setAccessible(true);
hack.set(null, true);
Fixitagain
fuente
44
Que mala idea. Intentaría obtener la fuente y recompilar (o incluso descompilar / recompilar) en su lugar.
Bill K
System.out es un campo final público estático, pero también se puede cambiar.
irreputable el
19
@irreputable System.out/in/errson tan "especiales" que el modelo de memoria de Java debe hacer una mención especial de ellos. No son ejemplos que deben seguirse.
Tom Hawtin - tackline
8
bueno, mi punto es encontrar un truco en el medio para que mi aplicación funcione hasta que el responsable de la biblioteca haga el cambio en la próxima versión, así que no necesito piratear más ...
solucione el
1
@Bill K de hace diez años: ¡sería GENIAL recompilarlo, pero está en un sistema implementado y solo necesito parchearlo hasta que podamos actualizar la aplicación implementada!
Bill K

Respuestas:

888

Suponiendo que no SecurityManagerte impide hacer esto, puedes usarlo setAccessiblepara desplazarte privatey restablecer el modificador para deshacerte de él finaly, de hecho, modificar un private static finalcampo.

Aquí hay un ejemplo:

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

Asumiendo que no SecurityExceptionse lanza, se imprime el código anterior "Everything is true".

Lo que realmente se hace aquí es lo siguiente:

  • Los booleanvalores primitivos truey falseen mainse autoboxing al tipo de referencia Boolean"constantes" Boolean.TRUEyBoolean.FALSE
  • La reflexión se utiliza para cambiar la public static final Boolean.FALSEreferencia a la Booleanreferencia deBoolean.TRUE
  • Como resultado, posteriormente cada vez que un falsese autoboxed a Boolean.FALSE, se refiere a la misma Booleancomo el referido a porBoolean.TRUE
  • Todo lo que era "false"ahora es"true"

Preguntas relacionadas


Advertencias

Se debe tener mucho cuidado cuando haga algo como esto. Es posible que no funcione porque SecurityManagerpuede estar presente, pero incluso si no lo hace, dependiendo del patrón de uso, puede funcionar o no.

JLS 17.5.3 Modificación posterior de campos finales

En algunos casos, como la deserialización, el sistema necesitará cambiar los finalcampos de un objeto después de la construcción. finallos campos se pueden cambiar a través de la reflexión y otros medios dependientes de la implementación. El único patrón en el que esto tiene una semántica razonable es uno en el que se construye un objeto y luego finalse actualizan los campos del objeto. El objeto no debe hacerse visible para otros subprocesos, ni deben finalleerse los campos, hasta que finalse completen todas las actualizaciones de los campos del objeto. Las congelaciones de un finalcampo se producen tanto al final del constructor en el que finalse establece el campo como inmediatamente después de cada modificación de un finalcampo a través de la reflexión u otro mecanismo especial.

Incluso entonces, hay una serie de complicaciones. Si un finalcampo se inicializa a una constante de tiempo de compilación en la declaración de campo, es finalposible que no se observen cambios en el campo, ya que los usos de ese finalcampo se reemplazan en tiempo de compilación con la constante de tiempo de compilación.

Otro problema es que la especificación permite una optimización agresiva de los finalcampos. Dentro de un hilo, está permitido reordenar las lecturas de un finalcampo con las modificaciones de un campo final que no tienen lugar en el constructor.

Ver también

  • JLS 15.28 Expresión constante
    • Es poco probable que esta técnica funcione con una primitiva private static final boolean, porque es inlineable como una constante de tiempo de compilación y, por lo tanto, el valor "nuevo" puede no ser observable

Apéndice: sobre la manipulación bit a bit

Esencialmente,

field.getModifiers() & ~Modifier.FINAL

apaga el bit correspondiente a Modifier.FINALfrom field.getModifiers(). &es el bit a bit y ~el complemento a bit.

Ver también


Recuerda expresiones constantes

¿Aún no puedes resolver esto ?, ¿has caído en la depresión como lo hice yo? ¿Su código se ve así?

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

Al leer los comentarios sobre esta respuesta, especialmente la de @Pshemo, me recordó que las expresiones constantes se manejan de manera diferente, por lo que será imposible modificarla. Por lo tanto, deberá cambiar su código para que se vea así:

public class A {
    private final String myVar;

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

si no eres el dueño de la clase ... te siento!

Para más detalles sobre por qué este comportamiento lee esto ?

poligenelubricantes
fuente
41
@thecoop, @HalfBrian: no hay duda de que esto es EXTREMADAMENTE MALO , pero este ejemplo fue elegido por diseño. Mi respuesta solo muestra cómo, en algunas circunstancias, esto es posible. El ejemplo más repugnante en el que puedo pensar es elegido deliberadamente con la esperanza de que quizás la gente se disguste instantáneamente en lugar de enamorarse de la técnica.
polygenelubricants
59
Hola Te escuché como reflejo, así que reflexioné en el campo para que puedas reflexionar mientras reflexionas.
Matthew Flaschen
11
Tenga en cuenta que Boolean.FALSE no es privado. ¿Esto realmente funciona con miembros "privados estáticos finales"?
mgaert
15
@mgaert lo hace, pero usted tiene que utilizar getDeclaredField()en lugar de getField()para la clase de objetivo
EIS
11
+1. Para aquellos que intentarán cambiar algo como final String myConstant = "x";y fallarán: recuerden que las constantes de tiempo de compilación serán alineadas por el compilador, de modo que cuando escriban código como System.out.println(myConstant);si fuera compilado como System.out.println("x");porque el compilador conoce el valor de la constante en el momento de la compilación. Para deshacerse de este problema, debe inicializar sus constantes en tiempo de ejecución como final String myConstant = new String("x");. También en caso de primitivas como el final int myField = 11uso final int myField = new Integer(11);ofinal Integer myField = 11;
Pshemo
58

Si el valor asignado a un static final booleancampo se conoce en tiempo de compilación, es una constante. Los campos de primitivo o Stringtipo pueden ser constantes de tiempo de compilación. Se incluirá una constante en cualquier código que haga referencia al campo. Como el campo no se lee realmente en tiempo de ejecución, cambiarlo no tendrá ningún efecto.

La especificación del lenguaje Java dice esto:

Si un campo es una variable constante (§4.12.4), eliminar la palabra clave final o cambiar su valor no interrumpirá la compatibilidad con los binarios preexistentes al hacer que no se ejecuten, pero no verán ningún valor nuevo para el uso del campo a menos que se vuelvan a compilar. Esto es cierto incluso si el uso en sí no es una expresión constante en tiempo de compilación (§15.28)

Aquí hay un ejemplo:

class Flag {
  static final boolean FLAG = true;
}

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

Si descompila Checker, verá que en lugar de hacer referencia Flag.FLAG, el código simplemente empuja un valor de 1 ( true) en la pila (instrucción # 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
erickson
fuente
Ese fue mi primer pensamiento, pero luego recordé Java compilado en tiempo de ejecución, si tuviera que restablecer el bit, simplemente lo volvería a compilar como una variable en lugar de una constante.
Bill K
44
@ Bill K - No, esto no se refiere a la compilación JIT. Los archivos de clase dependientes contendrán los valores en línea y no harán referencia a la clase independiente. Es un experimento bastante simple de probar; Agregaré un ejemplo.
erickson
1
¿Cómo funciona esto con la respuesta de @polygenelubricants donde redefine Boolean.false? - pero tienes razón, he visto este comportamiento cuando las cosas no se volvieron a compilar correctamente.
Bill K
26
@ Bill K: en la respuesta de polygenlubricants, el campo no es una constante de tiempo de compilación. Es public static final Boolean FALSE = new Boolean(false)nopublic static final boolean FALSE = false
Erickson
17

Un poco de curiosidad de la Especificación del lenguaje Java, capítulo 17, sección 17.5.4 "Campos protegidos contra escritura":

Normalmente, un campo que es final y estático no puede modificarse. Sin embargo, System.in, System.out y System.err son campos finales estáticos que, por razones heredadas, deben permitirse cambiar mediante los métodos System.setIn, System.setOut y System.setErr. Nos referimos a estos campos como protegidos contra escritura para distinguirlos de los campos finales ordinarios.

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

Stephan Markwalder
fuente
9

También lo integré con joor library

Solo usa

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

También solucioné un problema con el overrideque las soluciones anteriores parecen fallar. Sin embargo, use esto con mucho cuidado, solo cuando no haya otra buena solución.

iirekm
fuente
Cuando intento esto (JDK12), obtengo una excepción: "No se puede establecer el campo final ___".
Aaron Iba
@AaronIba Ya no está permitido en Java 12+.
Nates
7

Junto con la respuesta mejor clasificada, puede utilizar un enfoque un poco más simple. La FieldUtilsclase commons de Apache ya tiene un método particular que puede hacer las cosas. Por favor, eche un vistazo al FieldUtils.removeFinalModifiermétodo. Debe especificar la instancia de campo de destino y la marca de forzamiento de accesibilidad (si juega con campos no públicos). Más información puedes encontrar aquí .

nndru
fuente
Esta es una solución mucho más simple que la respuesta actualmente aceptada
Bernie
44
¿Lo es? Copiar un método suena como una solución más simple que importar una biblioteca completa (que está haciendo lo mismo que el método que estaría copiando).
eski
1
No funciona en Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR
6

En caso de presencia de un administrador de seguridad, uno puede hacer uso de AccessController.doPrivileged

Tomando el mismo ejemplo de la respuesta aceptada arriba:

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

En la expresión lambda AccessController.doPrivileged, se puede simplificar a:

AccessController.doPrivileged((PrivilegedAction) () -> {
    modifiersField.setAccessible(true);
    return null;
});
VanagaS
fuente
1
Esto tampoco parece funcionar con Java 12+.
dan1st
sí @ dan1st, tienes razón! Compruebe esto para obtener una solución: stackoverflow.com/a/56043252/2546381
VanagaS
2

La respuesta aceptada funcionó para mí hasta que se implementó en JDK 1.8u91. Luego me di cuenta de que fallaba en la field.set(null, newValue);línea cuando había leído el valor mediante reflexión antes de llamar al setFinalStaticmétodo.

Probablemente la lectura causó de alguna manera una configuración diferente de los elementos internos de reflexión de Java (es decir, sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImplen caso de error en lugar de sun.reflect.UnsafeStaticObjectFieldAccessorImplen caso de éxito), pero no lo desarrollé más.

Como necesitaba establecer temporalmente un nuevo valor basado en el valor anterior y luego volver a establecer el valor anterior, cambié un poco la firma para proporcionar externamente la función de cálculo y también devolver el valor anterior:

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

Sin embargo, para un caso general, esto no sería suficiente.

Tomáš Záluský
fuente
2

Incluso a pesar de ser final un campo puede modificarse fuera del inicializador estático y (al menos JVM HotSpot) ejecutará el código de bytes perfectamente bien.

El problema es que el compilador de Java no permite esto, pero esto puede pasarse por alto fácilmente objectweb.asm. Aquí hay un archivo de clase perfectamente válido que pasa la verificación de bytecode y se carga e inicializa con éxito en 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 clase se ve más o menos como sigue:

public class Cl{
    private static final int fld;

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

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

que no se puede compilar con javac , pero JVM puede cargar y ejecutar.

JVM HotSpot tiene un tratamiento especial de tales clases en el sentido de que evita que tales "constantes" participen en el plegado constante. Esta comprobación se realiza en la fase de reescritura de bytecode de inicialización de clase :

// 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 única restricción que comprueba JVM HotSpot es que el finalcampo no debe modificarse fuera de la clase en la que finalse declara el campo.

Algun nombre
fuente
0

Acabo de ver esa pregunta en una de las preguntas de la entrevista, si es posible cambiar la variable final con reflexión o en tiempo de ejecución. Me interesé mucho, así que con lo que me convertí:

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

Alguna clase simple con la variable de cadena final. Entonces, en la clase principal import 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);
    }
}

El resultado será el siguiente:

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

De acuerdo con la documentación https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html

Hasskell
fuente
¿Has visto esta publicación?
Ravindra HV
Esta pregunta se refiere a un staticcampo final, por lo que este código no funciona. setAccessible(true)solo funciona para establecer campos de instancia final.
Radiodef
0

Si su campo es simplemente privado, puede hacer esto:

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

y lanzar / manejar NoSuchFieldException

Philip Rego
fuente
-4

Todo el punto de un final campo es que no se puede reasignar una vez establecido. La JVM utiliza esta garantía para mantener la coherencia en varios lugares (por ejemplo, clases internas que hacen referencia a variables externas). Entonces no. ¡Ser capaz de hacerlo rompería la JVM!

La solución es no declararlo finalen primer lugar.

thecoop
fuente
44
Además, finaltiene un papel especial en la ejecución multiproceso: cambiar los finalvalores también rompería el modelo de memoria Java.
Péter Török
1
Y un campo no declarado finalno debe ser declarado static.
Tom Hawtin - tackline
2
@Tom: En general, eso probablemente sea cierto, pero no prohibiría todas las variables mutables estáticas.
bcat
77
@Tom: ¿Alguna vez leíste por qué los solteros son malvados? ¡Yo hice! Ahora sé que solo son malvados en Java. Y solo debido a la disponibilidad de un cargador de clases definido por el usuario. Y desde que sé todo esto y no uso el cargador de clases definido por el usuario, uso singletons sin arrepentimiento. Y también Scala, donde el singleton es una característica del lenguaje de primera clase: que los singletons son malvados es un mito falso bien conocido .
Martin
3
@ Martin Sé que tu comentario es antiguo, y tal vez tus puntos de vista han cambiado por ahora, pero pensé que solo agregaría esto: los Singletons son malos por razones que no tienen nada que ver con Java. Añaden complejidad oculta a su código. Además, pueden hacer que sea imposible realizar pruebas unitarias sin saber también que n singletons también deben configurarse primero. Son la antítesis de la inyección de dependencia. Su equipo puede tomar la decisión de que las trampas de tener una complejidad oculta no superan la conveniencia de Singletons, pero muchos equipos toman la postura opuesta por una buena razón.
enamorado