Змініть приватне статичне підсумкове поле за допомогою відображення Java


479

У мене є клас із private static finalполем, яке, на жаль, мені потрібно змінити під час виконання.

Використовуючи рефлексію, я отримую цю помилку: java.lang.IllegalAccessException: Can not set static final boolean field

Чи є спосіб змінити значення?

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

4
Така погана ідея. Я б спробував замість цього отримати джерело та перекомпілювати (або навіть декомпілювати / перекомпілювати).
Білл К

System.out - це загальнодоступне статичне підсумкове поле, але воно також може бути змінено.
незаперечний

19
@irreputable System.out/in/errнастільки "особливі", що модель пам'яті Java повинна спеціально згадувати про них. Вони не є прикладами, яких слід наслідувати.
Том Хоутін - тайклін

8
ну мій сенс знайти хак між тим, щоб моя програма працювала до тих пір, поки відповідальна
влада

1
@Bill K від десяти років тому: було б ВЕЛИКЕ перекомпілювати його, але він знаходиться у розгорнутій системі, і мені просто потрібно виправити його, поки ми не зможемо оновити розгорнуту програму!
Білл К

Відповіді:


888

Якщо ні, що SecurityManagerце не заважає вам це робити, ви можете використовувати, setAccessibleщоб обійти privateта скинути модифікатор, щоб позбутися finalта фактично змінити private static finalполе.

Ось приклад:

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

Якщо припустимо, що "не SecurityExceptionкинуто", наведений вище код друкується "Everything is true".

Тут насправді зроблено наступне:

  • Примітивні booleanзначення trueі falseв mainвводяться в автоматичне позначення типу Boolean"константи" Boolean.TRUEтаBoolean.FALSE
  • Рефлексія використовується для зміни public static final Boolean.FALSEпозначення, на яке BooleanпосилаєтьсяBoolean.TRUE
  • Як наслідок, згодом щоразу, коли a автоматично falseзавантажується Boolean.FALSE, воно посилається на те саме, що Booleanі те , на яке посилаєтьсяBoolean.TRUE
  • Все, що було "false"зараз, є"true"

Пов'язані питання


Коваджі

Ви повинні бути дуже обережними, коли ви робите щось подібне. Він може не працювати, тому що SecurityManagerможе бути присутнім, але навіть якщо він не працює, залежно від схеми використання, він може працювати, а може і не працювати.

JLS 17.5.3 Подальші зміни остаточних полів

У деяких випадках, таких як десеріалізація, системі потрібно буде змінити finalполя об’єкта після побудови. finalполя можуть бути змінені за допомогою відображення та інших засобів, що залежать від реалізації. Єдиний зразок, в якому це має розумну семантику, - це той, в якому будується об’єкт і потім finalоновлюються поля об’єкта. Об'єкт не повинен бути видимим для інших потоків, а також finalполя читання, поки не будуть завершені всі оновлення finalполів об’єкта. Замороження finalполя відбуваються як в кінці конструктора, в якому встановлено finalполе, так і відразу після кожної модифікації finalполя за допомогою відображення або іншого спеціального механізму.

Вже тоді є ряд ускладнень. Якщо finalполе ініціалізується до константи часу компіляції в декларації поля, зміни в finalполі можуть не спостерігатися, оскільки використання цього finalполя замінюються під час компіляції на постійну часу компіляції.

Ще одна проблема полягає в тому, що специфікація дозволяє агресивно оптимізувати finalполя. У межах потоку допустимо впорядкувати зчитування finalполя з тими модифікаціями остаточного поля, які не відбуваються в конструкторі.

Дивись також

  • JLS 15.28 Постійна експресія
    • Малоймовірно, що ця методика працює з примітивом private static final boolean, тому що вона невід'ємна як константа часу компіляції, і, отже, "нове" значення може не спостерігатися

Додаток: Про побітну маніпуляцію

По суті,

field.getModifiers() & ~Modifier.FINAL

вимикає біт , відповідний Modifier.FINALз field.getModifiers(). &є побитовим і, і ~є порозрядним доповненням.

Дивись також


Запам’ятайте постійні вирази

Ще не в змозі вирішити це? - впали в депресію, як я це зробив? Чи виглядає ваш код таким чином?

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

Читаючи коментарі до цієї відповіді, зокрема, від @Pshemo, це нагадало мені, що з постійними виразами обробляються різні, тому змінити її буде неможливо . Отже, вам потрібно буде змінити код, щоб він виглядав так:

public class A {
    private final String myVar;

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

якщо ти не власник класу ... я тебе відчуваю!

Детальніше про те, чому ця поведінка читає це ?


41
@thecoop, @HalfBrian: немає сумнівів у тому, що це НАЙКРАЙНЕ ЗЛО , але цей приклад був обраний дизайном. Моя відповідь лише показує, як в деяких умовах це можливо. Найбільш огидним прикладом, який я можу придумати, є навмисне обраний з надією, що, можливо, люди будуть миттєво огидні, замість того, щоб полюбити техніку.
полігенмастильні матеріали

59
Йо, дауг. Я чув, що ти любиш рефлексію, тому я розмірковував над полем, щоб ти міг відобразити, поки ти роздумуєш.
Метью Флашен

11
Зауважте, що Boolean.FALSE не є приватним. Чи справді це працює з членами "приватних остаточних статичних"?
mgaert

15
@mgaert це робить, але ви повинні використовувати getDeclaredField()замість getField()для цільового класу
EIS

11
+1. Для тих, хто намагатиметься змінити щось на кшталт final String myConstant = "x";і не вдасться: пам’ятайте, що константи часу компіляції будуть позначені компілятором, тому коли ви будете писати код, System.out.println(myConstant);він буде складений так, System.out.println("x");тому що компілятор знає значення константи під час компіляції. Щоб позбутися цієї проблеми, вам потрібно ініціалізувати константи на час виконання final String myConstant = new String("x");. Також у випадку примітивів, таких як final int myField = 11використання final int myField = new Integer(11);абоfinal Integer myField = 11;
Пшемо

58

Якщо значення, призначене для static final booleanполя, відоме під час компіляції, це константа. Поля примітиву чи Stringтипу можуть бути константами часу компіляції. Константа буде вкладена в будь-який код, на який посилається поле. Оскільки поле насправді не читається під час виконання, його зміна не матиме жодного ефекту.

Специфікація мови Java говорить про це:

Якщо поле є постійною змінною (§4.12.4), вилучення ключового слова остаточне або зміна його значення не порушить сумісність із попередньо існуючими бінарними файлами, змусивши їх не працювати, але вони не побачать нового значення для використання поля, якщо вони не перекомпільовані. Це справедливо, навіть якщо саме використання не є постійним виразом часу компіляції (§15.28)

Ось приклад:

class Flag {
  static final boolean FLAG = true;
}

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

Якщо декомпілювати Checker, ви побачите, що замість посилань Flag.FLAGкод просто висуває trueна стек значення 1 ( ) (інструкція №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

Це була моя перша думка, але потім я згадав Java, складену під час виконання, якщо ви скинули біт, він просто перекомпілюватиметься з нею як змінною, а не константою.
Білл К

4
@Bill K - Ні, це не стосується компіляції JIT. Файли залежних класів насправді будуть містити вкладені значення та ніякого посилання на незалежний клас. Це досить простий експеримент для тестування; Додам приклад.
erickson

1
Як це джив з відповіддю @polygenelubricants, де він переосмислює Boolean.false?
Білл К

26
@Bill K - у відповіді полігенмастильних матеріалів поле не є постійною часом компіляції. Це public static final Boolean FALSE = new Boolean(false)не такpublic static final boolean FALSE = false
erickson

17

Трохи цікавості у специфікації мови Java, глава 17, розділ 17.5.4 "Поля, захищені від запису":

Зазвичай поле, яке є остаточним і статичним, може не змінюватися. Однак System.in, System.out і System.err - це статичні кінцеві поля, які з застарілих причин повинні бути дозволені зміною методами System.setIn, System.setOut та System.setErr. Ми називаємо ці поля захищеними від запису, щоб відрізняти їх від звичайних кінцевих полів.

Джерело: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.4


9

Я також інтегрував його в бібліотеку прихожих

Просто використовуйте

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

Також я вирішив проблему, з overrideякою, здається, не було попередніх рішень. Однак використовуйте це дуже обережно, лише тоді, коли іншого хорошого рішення немає.


Коли я спробую це (JDK12), я отримую виняток: "Неможливо встановити остаточне поле ___".
Аарон Іба

@AaronIba Це більше не дозволено в Java 12+.
NateS

7

Поряд з найвищим рейтингом відповіді, ви можете використовувати трохи найпростіший підхід. FieldUtilsКлас Apache commons вже має особливий метод, який може виконувати завдання. Будь ласка, погляньте на FieldUtils.removeFinalModifierметод. Ви повинні вказати екземпляр цільового поля та прапор, що примушує доступ (якщо ви граєте з непублічними полями). Більше інформації ви можете знайти тут .


Це набагато простіше рішення, ніж прийнята на даний момент відповідь
Берні

4
Є це? Копіювання одного методу звучить як простіше рішення, ніж імпорт цілої бібліотеки (що робить те саме, що і метод, який ви копіювали).
eski

1
Не працює на Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR

6

У разі наявності менеджера з безпеки можна скористатися AccessController.doPrivileged

Беручи той самий приклад із прийнятої відповіді вище:

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

В виразі лямбда AccessController.doPrivileged, можна спростити:

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

1
Це також не працює з Java 12+.
dan1st

так @ dan1st, ви праві! Будь ласка , перевірте це для вирішення: stackoverflow.com/a/56043252/2546381
Ванагас

2

Прийнята відповідь працювала для мене до моменту розгортання на JDK 1.8u91. Тоді я зрозумів, що це не вдалося в field.set(null, newValue);черзі, коли я прочитав значення за допомогою рефлексії перед викликомsetFinalStatic методу.

Можливо, прочитане спричинило дещо інше налаштування інтерфейсів відображення Java (а саме sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImplу відмовних випадках замість)sun.reflect.UnsafeStaticObjectFieldAccessorImpl успішного випадку), але я не деталізував це далі.

Оскільки мені потрібно було тимчасово встановити нове значення на основі старого значення та пізніше встановити старе значення назад, я трохи змінив підпис, щоб забезпечити функцію обчислення зовні, а також повернути старе значення:

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

Однак для загального випадку цього буде недостатньо.


2

Навіть незважаючи на буття final що поле може бути змінено поза статичним ініціалізатором і (принаймні, JVM HotSpot) виконає байт-код ідеально добре.

Проблема полягає в тому, що компілятор Java цього не дозволяє, але це можна легко обійти objectweb.asm. Тут ідеально правильний файл класу, який проходить перевірку байтового коду та успішно завантажується та ініціалізується під 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();

У Java клас виглядає приблизно так:

public class Cl{
    private static final int fld;

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

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

яку неможливо скласти javac , але може бути завантажена та виконана JVM.

JVM HotSpot має спеціальну обробку таких класів у тому сенсі, що заважає таким «константам» брати участь у постійному складенні. Ця перевірка проводиться на етапі перезапису байт-коду ініціалізації класу :

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

Єдине обмеження, яке перевіряє JVM HotSpot, - це те, що finalполе не слід змінювати за межами класу, в якому оголошено finalполе.


0

Щойно побачив це питання на одному з питань інтерв'ю, якщо можливо, змінити остаточну змінну з відображенням або під час виконання. Мені дуже цікаво, так що я став із цим:

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

Деякі прості класи з остаточною змінною String. Так у основний клас імпортують 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);
    }
}

Вихід буде таким:

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

Відповідно до документації https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html


Ви бачили цю публікацію?
Равіндра HV

Це запитання задає staticостаточне поле, тому цей код не працює. setAccessible(true)працює лише для встановлення кінцевих полів примірника.
Radiodef

0

Якщо ваше поле просто приватне, ви можете це зробити:

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

і кинути / обробляти NoSuchFieldException


-4

Вся суть а final поля полягає в тому, що воно не може бути перепризначене після встановлення. JVM використовує цю гарантію для підтримки послідовності в різних місцях (наприклад, внутрішні класи, що посилаються на зовнішні змінні). Так ні. Бути в змозі зробити це зламає СВМ!

Рішення полягає не в тому, щоб заявляти про це finalв першу чергу.


4
Крім того, finalвідіграє особливу роль у багатопотоковому виконанні - зміна finalзначень також порушить модель пам'яті Java.
Péter Török

1
І поле, яке не було оголошено, finalне повинно бути оголошено static.
Том Хотін - тайклін

2
@Tom: Загалом це, мабуть, правда, але я не заборонив би всі статичні змінні змінні.
bcat

7
@Tom: Ви коли-небудь читали, чому одинаки злі? Я зробив! Тепер я знаю, що вони злі лише на Яві. І тільки через доступність завантажувача, визначеного користувачем. І відтоді, як я все це знаю, і не використовую завантажувач, визначений користувачем, я без жалю використовую одиночні кнопки. Так само і Скала, де одиночні є особливістю мови першого класу - про те, що одинаки є злими, є добре відомим помилковим міфом .
Мартін

3
@Martin Я знаю, що ваш коментар старий, і, можливо, ваші погляди змінилися до цього часу, але я подумав, що я просто додам це: Singletons є злими з причин, які не мають нічого спільного з Java. Вони додають вашому коду приховану складність . Крім того, вони можуть унеможливити тестування одиниць, не знаючи також, що спочатку повинні бути налаштовані й n одиночних клавіш. Вони є антитезою ін'єкції залежності. Ваша команда може прийняти рішення, що підводні камені прихованої складності не переважують зручності Сінглтона, але багато команд з поважної причини займають протилежну позицію.
розчавлення
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.