Thay đổi trường cuối cùng tĩnh riêng bằng cách sử dụng phản chiếu Java


479

Tôi có một lớp học với một private static finallĩnh vực mà thật không may, tôi cần thay đổi nó vào thời gian chạy.

Sử dụng sự phản chiếu tôi nhận được lỗi này: java.lang.IllegalAccessException: Can not set static final boolean field

Có cách nào để thay đổi giá trị?

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

4
Thật là một ý tưởng tồi. Thay vào đó, tôi sẽ cố gắng lấy nguồn và biên dịch lại (hoặc thậm chí dịch ngược / biên dịch lại).
Bill K

System.out là trường cuối cùng tĩnh công khai, nhưng nó cũng có thể được thay đổi.
chối cãi

19
@irreputable System.out/in/err"đặc biệt" đến mức Mô hình bộ nhớ Java phải đề cập đặc biệt đến chúng. Chúng không phải là những ví dụ cần được tuân theo.
Tom Hawtin - tackline

8
Vâng, quan điểm của tôi là tìm một hack ở giữa để ứng dụng của tôi hoạt động cho đến khi lib chịu trách nhiệm thực hiện thay đổi ở lần phát hành tiếp theo vì vậy tôi không cần phải hack nữa ...
fixitagain

1
@Bill K từ mười năm trước: Sẽ rất TUYỆT VỜI khi biên dịch lại nhưng nó trên một hệ thống đã triển khai và tôi chỉ cần vá nó cho đến khi chúng tôi có thể cập nhật ứng dụng đã triển khai!
Bill K

Câu trả lời:


888

Giả sử không SecurityManagerngăn cản bạn làm điều này, bạn có thể sử dụng setAccessibleđể đi lại privatevà đặt lại công cụ sửa đổi để loại bỏ finalvà thực sự sửa đổi một private static finaltrường.

Đây là một ví dụ:

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

Giả sử không có SecurityExceptionném, mã trên in "Everything is true".

Những gì thực sự được thực hiện ở đây là như sau:

  • Các booleangiá trị nguyên thủy truefalsetrong mainđược tự động chuyển sang loại tham chiếu Boolean"hằng số" Boolean.TRUEBoolean.FALSE
  • Sự phản chiếu được sử dụng để thay đổi public static final Boolean.FALSEtham chiếu đến tham chiếu BooleanbởiBoolean.TRUE
  • Kết quả là, sau đó bất cứ khi nào a falseđược tự động chuyển đến Boolean.FALSE, nó đều giống Booleannhư cái được giới thiệu bởiBoolean.TRUE
  • Tất cả mọi thứ "false"bây giờ là"true"

Câu hỏi liên quan


Hãy cẩn thận

Cần hết sức cẩn thận bất cứ khi nào bạn làm điều gì đó như thế này. Nó có thể không hoạt động vì SecurityManagercó thể có mặt, nhưng ngay cả khi nó không, tùy thuộc vào mô hình sử dụng, nó có thể hoặc không thể hoạt động.

JLS 17.5.3 Sửa đổi tiếp theo của các trường cuối cùng

Trong một số trường hợp, chẳng hạn như khử lưu huỳnh, hệ thống sẽ cần thay đổi các finaltrường của một đối tượng sau khi xây dựng. finalcác trường có thể được thay đổi thông qua sự phản ánh và các phương tiện phụ thuộc thực hiện khác. Mẫu duy nhất trong đó có ngữ nghĩa hợp lý là một mẫu trong đó một đối tượng được xây dựng và sau đó các finaltrường của đối tượng được cập nhật. Không nên hiển thị đối tượng cho các luồng khác, cũng như các finaltrường không được đọc, cho đến khi tất cả các cập nhật cho các finaltrường của đối tượng hoàn tất. Sự đóng băng của một finaltrường xảy ra cả ở cuối của hàm tạo trong đó finaltrường được đặt và ngay sau mỗi lần sửa đổi một finaltrường thông qua sự phản chiếu hoặc cơ chế đặc biệt khác.

Thậm chí sau đó, có một số biến chứng. Nếu một finaltrường được khởi tạo thành hằng số thời gian biên dịch trong khai báo trường, các thay đổi đối với finaltrường có thể không được quan sát, vì việc sử dụng finaltrường đó được thay thế tại thời gian biên dịch với hằng số thời gian biên dịch.

Một vấn đề khác là đặc tả cho phép tối ưu hóa mạnh mẽ các finaltrường. Trong một luồng, cho phép sắp xếp lại các lần đọc của một finaltrường với những sửa đổi của trường cuối cùng không diễn ra trong hàm tạo.

Xem thêm

  • JLS 15.28 Biểu thức không đổi
    • Không chắc là kỹ thuật này hoạt động với nguyên thủy private static final boolean, bởi vì nó không thể điều chỉnh được dưới dạng hằng số thời gian biên dịch và do đó giá trị "mới" có thể không quan sát được

Phụ lục: Về thao tác bitwise

Bản chất,

field.getModifiers() & ~Modifier.FINAL

tắt bit tương ứng với Modifier.FINALtừ field.getModifiers(). &là bitwise-và, và ~là phần bù bit.

Xem thêm


Ghi nhớ biểu thức liên tục

Vẫn không thể giải quyết điều này?, Đã rơi vào trầm cảm như tôi đã làm cho nó? Mã của bạn trông như thế này?

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

Đọc các bình luận về câu trả lời này, đặc biệt là câu trả lời của @Pshemo, nó nhắc nhở tôi rằng Biểu thức liên tục được xử lý khác nhau nên sẽ không thể sửa đổi nó. Do đó, bạn sẽ cần thay đổi mã của mình để trông như thế này:

public class A {
    private final String myVar;

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

nếu bạn không phải là chủ sở hữu của lớp ... tôi cảm thấy bạn!

Để biết thêm chi tiết về lý do tại sao hành vi này đọc này ?


41
@thecoop, @HalfBrian: không còn nghi ngờ gì nữa, đây là TUYỆT VỜI , nhưng ví dụ này được chọn theo thiết kế. Câu trả lời của tôi chỉ cho thấy làm thế nào, trong một số trường hợp, điều này là có thể. Ví dụ kinh tởm nhất mà tôi có thể nghĩ đến là cố tình chọn với hy vọng rằng có lẽ mọi người sẽ ngay lập tức chán ghét thay vì yêu kỹ thuật này.
đa gen

59
Yo, dawg. Tôi nghe bạn thích phản xạ, vì vậy tôi đã phản ánh trên trường để bạn có thể phản ánh trong khi bạn phản ánh.
Matthew Flaschen

11
Lưu ý rằng Boolean.FALSE không riêng tư. Điều này có thực sự hoạt động với các thành viên "private private static" không?
mgaert

15
@mgaert thì có, nhưng bạn phải sử dụng getDeclaredField()thay vì getField()cho lớp mục tiêu
eis

11
+1. Đối với những người sẽ cố gắng thay đổi một cái gì đó giống final String myConstant = "x";và sẽ thất bại: hãy nhớ rằng các hằng số thời gian biên dịch sẽ được biên dịch bởi trình biên dịch để khi bạn viết mã như System.out.println(myConstant);nó sẽ được biên dịch System.out.println("x");vì trình biên dịch biết giá trị của hằng số tại thời gian biên dịch. Để thoát khỏi vấn đề này, bạn cần khởi tạo các hằng số của mình trong thời gian chạy như thế nào final String myConstant = new String("x");. Ngoài ra trong trường hợp người nguyên thủy như final int myField = 11sử dụng final int myField = new Integer(11);hoặcfinal Integer myField = 11;
Pshemo

58

Nếu giá trị được gán cho một static final booleantrường được biết tại thời gian biên dịch, thì đó là một hằng số. Các trường nguyên thủy hoặc Stringloại có thể là hằng số thời gian biên dịch. Một hằng số sẽ được nội tuyến trong bất kỳ mã nào tham chiếu trường. Vì trường không thực sự được đọc trong thời gian chạy, nên việc thay đổi nó sẽ không có hiệu lực.

Đặc tả ngôn ngữ Java nói điều này:

Nếu một trường là một biến không đổi (§4.12.4), thì việc xóa từ khóa cuối cùng hoặc thay đổi giá trị của nó sẽ không phá vỡ tính tương thích với các nhị phân có sẵn bằng cách khiến chúng không chạy, nhưng chúng sẽ không thấy bất kỳ giá trị mới nào cho việc sử dụng của trường trừ khi chúng được biên dịch lại. Điều này đúng ngay cả khi bản thân việc sử dụng không phải là biểu thức hằng số thời gian biên dịch (§15.28)

Đây là một ví dụ:

class Flag {
  static final boolean FLAG = true;
}

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

Nếu bạn dịch ngược Checker, bạn sẽ thấy rằng thay vì tham chiếu Flag.FLAG, mã chỉ cần đẩy giá trị 1 ( true) lên ngăn xếp (hướng dẫn số 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

Đó là suy nghĩ đầu tiên của tôi, nhưng sau đó tôi nhớ Java được biên dịch trong thời gian chạy, nếu bạn đặt lại bit, nó sẽ chỉ đơn giản biên dịch lại với nó như một biến thay vì hằng số.
Bill K

4
@Bill K - Không, điều này không đề cập đến việc biên dịch JIT. Các tệp lớp phụ thuộc sẽ thực sự chứa các giá trị được nội tuyến và không tham chiếu đến lớp độc lập. Đây là một thử nghiệm khá đơn giản để thử nghiệm; Tôi sẽ thêm một ví dụ.
erickson

1
Làm thế nào mà câu chuyện này với câu trả lời của @polygenelubricants khi anh định nghĩa lại Boolean.false? - nhưng bạn đã đúng, tôi đã thấy hành vi này khi mọi thứ không được biên dịch lại chính xác.
Bill K

26
@Bill K - trong câu trả lời của polygenlubricants, trường không phải là hằng số thời gian biên dịch. Không public static final Boolean FALSE = new Boolean(false)phảipublic static final boolean FALSE = false
erickson

17

Một chút tò mò từ Đặc tả ngôn ngữ Java, chương 17, phần 17.5.4 "Các trường được bảo vệ chống ghi":

Thông thường, một trường cuối cùng và tĩnh có thể không được sửa đổi. Tuy nhiên, System.in, System.out và System.err là các trường cuối cùng tĩnh, vì lý do cũ, phải được phép thay đổi bằng các phương thức System.setIn, System.setOut và System.setErr. Chúng tôi đề cập đến các trường này như được bảo vệ bằng văn bản để phân biệt chúng với các trường cuối cùng thông thường.

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


9

Tôi cũng tích hợp nó với thư viện joor

Chỉ dùng

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

Ngoài ra tôi đã sửa một vấn đề overridemà các giải pháp trước đó dường như bỏ lỡ. Tuy nhiên, sử dụng nó rất cẩn thận, chỉ khi không có giải pháp tốt khác.


Khi tôi thử điều này (JDK12), tôi nhận được một ngoại lệ: "Không thể đặt trường ___ cuối cùng".
Aaron Iba

@AaronIba Nó không còn được phép trong Java 12+.
NateS

7

Cùng với câu trả lời được xếp hạng hàng đầu, bạn có thể sử dụng một cách tiếp cận đơn giản nhất. FieldUtilsLớp commons Apache đã có phương thức cụ thể có thể thực hiện công cụ. Xin vui lòng, hãy xem FieldUtils.removeFinalModifierphương pháp. Bạn nên chỉ định thể hiện trường mục tiêu và cờ buộc khả năng truy cập (nếu bạn chơi với các trường không công khai). Thêm thông tin bạn có thể tìm thấy ở đây .


Đây là một giải pháp đơn giản hơn nhiều so với câu trả lời hiện được chấp nhận
Bernie

4
Là nó? Sao chép một phương thức nghe có vẻ là một giải pháp đơn giản hơn so với nhập toàn bộ thư viện (đang thực hiện tương tự như phương pháp bạn đang sao chép).
eski

1
Không hoạt động trong Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR

6

Trong trường hợp có sự hiện diện của Trình quản lý bảo mật, người ta có thể sử dụng AccessController.doPrivileged

Lấy ví dụ tương tự từ câu trả lời được chấp nhận ở trên:

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

Trong biểu thức lambda AccessController.doPrivileged, có thể được đơn giản hóa thành:

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

1
Điều này dường như cũng không hoạt động với java 12+.
dan1st

vâng @ dan1st, bạn nói đúng! Vui lòng kiểm tra điều này để biết giải pháp: stackoverflow.com/a/56043252/2546381
VanagaS

2

Câu trả lời được chấp nhận làm việc cho tôi cho đến khi được triển khai trên JDK 1.8u91. Sau đó, tôi nhận ra nó thất bại tại field.set(null, newValue);dòng khi tôi đã đọc giá trị thông qua sự phản chiếu trước khi gọi setFinalStaticphương thức.

Có lẽ việc đọc gây ra một số thiết lập khác nhau của các phần tử phản chiếu Java (cụ thể là sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpltrong trường hợp thất bại thay vì sun.reflect.UnsafeStaticObjectFieldAccessorImpltrong trường hợp thành công) nhưng tôi đã không giải thích thêm.

Vì tôi cần tạm thời đặt giá trị mới dựa trên giá trị cũ và sau đó đặt lại giá trị cũ, tôi đã thay đổi chữ ký một chút để cung cấp hàm tính toán bên ngoài và cũng trả về giá trị cũ:

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

Tuy nhiên đối với trường hợp chung thì điều này là không đủ.


2

Ngay cả khi là finalmột trường có thể được sửa đổi bên ngoài bộ khởi tạo tĩnh và (ít nhất là JVM HotSpot) sẽ thực thi mã byte hoàn toàn tốt.

Vấn đề là trình biên dịch Java không cho phép điều này, nhưng điều này có thể dễ dàng bỏ qua bằng cách sử dụng objectweb.asm. Đây là classfile hoàn toàn hợp lệ vượt qua xác minh mã byte và được tải và khởi tạo thành công theo 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();

Trong Java, lớp trông đại khái như sau:

public class Cl{
    private static final int fld;

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

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

không thể được biên dịch với javac, nhưng có thể được tải và thực thi bởi JVM.

JVM HotSpot có cách xử lý đặc biệt đối với các lớp như vậy theo nghĩa là nó ngăn "các hằng số" đó tham gia vào việc gấp liên tục. Kiểm tra này được thực hiện trên giai đoạn viết lại mã byte của khởi tạo lớp :

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

Hạn chế duy nhất mà JVM HotSpot kiểm tra là finalkhông nên sửa đổi trường bên ngoài lớp mà finaltrường được khai báo tại.


0

Chỉ cần xem câu hỏi đó trên một trong những câu hỏi phỏng vấn, nếu có thể để thay đổi biến cuối cùng với sự phản ánh hoặc trong thời gian chạy. Đã thực sự quan tâm, vì vậy những gì tôi đã trở thành:

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

Một số lớp đơn giản với biến Chuỗi cuối cùng. Vì vậy, trong lớp chính nhập 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);
    }
}

Đầu ra sẽ như sau:

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

Theo tài liệu https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html


Bạn đã thấy bài này ?
Ravindra HV

Câu hỏi này hỏi về một statictrường cuối cùng, vì vậy mã này không hoạt động. setAccessible(true)chỉ hoạt động để thiết lập các trường mẫu cuối cùng.
Radiodef

0

Nếu lĩnh vực của bạn chỉ đơn giản là riêng tư, bạn có thể làm điều này:

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

và ném / xử lý NoSuchFieldException


-4

Toàn bộ điểm của một finaltrường là nó không thể được gán lại sau khi đã đặt. JVM sử dụng guarentee này để duy trì tính nhất quán ở nhiều nơi khác nhau (ví dụ: các lớp bên trong tham chiếu các biến ngoài). Vì vậy, không. Có thể làm như vậy sẽ phá vỡ JVM!

Giải pháp là không khai báo nó finalở nơi đầu tiên.


4
Hơn nữa, finalcó một vai trò đặc biệt trong thực thi đa luồng - finalcác giá trị thay đổi cũng sẽ phá vỡ mô hình bộ nhớ Java.
Péter Török

1
Và một lĩnh vực không được khai báo finalkhông nên được khai báo static.
Tom Hawtin - tackline

2
@Tom: Nói chung điều đó có thể đúng, nhưng tôi sẽ không cấm tất cả các biến có thể thay đổi tĩnh.
bcat

7
@Tom: Bạn đã bao giờ đọc lên tại sao singletons là ác? Tôi đã làm! Bây giờ tôi biết rằng họ chỉ ác trong Java. Và chỉ vì tính khả dụng của trình nạp lớp do người dùng định nghĩa. Và kể từ khi tôi biết tất cả những điều này và tôi không sử dụng trình nạp lớp do người dùng định nghĩa, tôi sử dụng singletons mà không hối tiếc. Và Scala cũng vậy, nơi singleton là một tính năng ngôn ngữ hạng nhất - Những người độc thân đó là ác là một huyền thoại sai lầm nổi tiếng .
Martin

3
@Martin Tôi biết rằng nhận xét của bạn đã cũ và có lẽ bây giờ quan điểm của bạn đã thay đổi, nhưng tôi nghĩ tôi chỉ cần thêm điều này: Singletons là xấu xa vì những lý do không liên quan đến Java. Họ thêm sự phức tạp ẩn vào mã của bạn. Ngoài ra, họ có thể không thể kiểm tra đơn vị mà không cần biết rằng n singletons cũng phải được cấu hình trước. Chúng là phản đề của tiêm phụ thuộc. Nhóm của bạn có thể đưa ra quyết định rằng những cạm bẫy của việc có sự phức tạp tiềm ẩn không vượt quá sự thuận tiện của Singletons, nhưng nhiều đội có lập trường ngược lại vì lý do chính đáng.
nghiền nát
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.