Java的非法反射警告illegal reflective access operation


反射是一項相當強大的特性,不僅在各類框架中被廣泛應用,即使是在日常開發中我們也隔三差五得要和它打交道。然而在JDK9中JDK對反射加上了一些限制,需要注意。

考慮有如下的代碼:

import java.lang.reflect.Field;
import java.util.ArrayList;

public class TestReflect {
    public static int getCapacity(ArrayList<?> l) throws Exception {
        Field dataField = l.getClass().getDeclaredField("elementData");
        dataField.setAccessible(true); // 即使設置了可訪問也會觸發警告
        return ((Object[]) dataField.get(l)).length; // 注意這行
    }

    public static void main(String[] args) {
        var arr = new ArrayList<Integer>(4);
        try {
            System.out.println("capacity:" + TestReflect.getCapacity(arr));        
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這段代碼的作用是讀取ArrayList的實際容量,由於JDK並沒有為我們提供類似cap()這樣的公開接口,所以我們不得不使用反射來繞過限制。

在JDK8上這段代碼運行良好,然而當我們升級成JDK9后卻會是這樣的一幅畫面:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by TestReflect (file:/tmp/TestReflect.java) to field java.util.ArrayList.elementData
WARNING: Please consider reporting this to the maintainers of TestReflect
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
capacity:4

別緊張,代碼還是正常運行了。其實這是JDK9中添加的新特性,即reflect不再可以訪問non-public成員以及不可公開訪問的class,原先這些訪問控制雖然存在但是可以通過reflect繞過,從JDK9開始反射也將遵循訪問控制的規則。JDK9中對於第一次訪問非公開成員的操作會顯示警告信息,我們可以通過``選項進一步顯示出有用的warning提示:

$ java --illegal-access=warn TestReflect.java

WARNING: Illegal reflective access by TestReflect (file:/tmp/TestReflect.java) to field java.util.ArrayList.elementData
capacity:4

將warn替換為debug可以指出非法訪問發生在哪一行(通過觀察打印的調用堆棧),而替換為deny則將會直接拋出java.lang.reflect.InaccessibleObjectException異常:

$ java --illegal-access=deny TestReflect.java

java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.lang.Object[] java.util.ArrayList.elementData accessible: module java.base does not "opens java.util" to unnamed module @72967906
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:349)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:289)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:174)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:168)
        at TestReflect.getCapacity(TestReflect.java:7)
        at TestReflect.main(TestReflect.java:14)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:415)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)

對於后續的更新版本,JDK會將deny作為默認的行為,不過目前(14.0.2)默認行為依舊為permit(顯示那些默認的warning信息)。

顯而易見,新特性會導致如下的結果:

  1. 無法再通過反射修改/訪問其他類型的私有成員;
  2. 無法再通過反射使用internal APIs

然而健康的代碼並不應該依賴於非法的訪問:

  1. 標記為非公開的成員本身就不希望被外部直接訪問,訪問應該通過public APIs進行;
  2. 無視訪問控制會導致數據的修改變得不可控,從而產生意想不到的缺陷;
  3. 私有成員可能會隨着開發/更新/修復/重構等活動而改變,過度依賴於訪問其他class的私有成員會產生高耦合性的代碼,使維護的負擔成倍增加

當然,想要消除警告也很容易,雖然官方文檔中不建議無視或消除這個警告:

$ java --add-opens java.base/java.util=ALL-UNNAMED TestReflect.java

capacity:4

--add-opens選項將特定的module下的package公開給制定的package,或者使用特殊值ALL-UNAMED代指所有匿名包(比如本例中的類),公開后我們就可以通過reflect來訪問被公開包中class的成員而不拋出異常了(需要使用setAccessible(true)

這種trick實際上嚴重破壞了代碼的封裝,個人是極不推薦的;此外還可以選擇將JDK版本鎖定在8,當然這也是不得以才為之的辦法,那么對於即想嘗試新版本又不想被警告信息煩擾的話該怎么辦呢,只能嘗試下面幾種辦法了:

  1. 如果是第三方依賴導致的警告,升級對應的依賴或是聯系該依賴的維護人員,請他們盡快修復問題。通常來說這類向后兼容的問題修復無需花費很多的時間。
  2. 如果是自己的代碼,則考慮是否要對私有成員提供可訪問的public API,是否將私有成員公有化。
  3. 如果使用了某些internal apis,那么你的代碼應該重構以避免過度依賴這些api。
參考資料

https://blog.codefx.org/java/java-9-migration-guide/#Illegal-Access-To-Internal-APIs
http://mail.openjdk.java.net/pipermail/jigsaw-dev/2017-June/012841.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM