反射是一項相當強大的特性,不僅在各類框架中被廣泛應用,即使是在日常開發中我們也隔三差五得要和它打交道。然而在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信息)。
顯而易見,新特性會導致如下的結果:
- 無法再通過反射修改/訪問其他類型的私有成員;
- 無法再通過反射使用internal APIs
然而健康的代碼並不應該依賴於非法的訪問:
- 標記為非公開的成員本身就不希望被外部直接訪問,訪問應該通過public APIs進行;
- 無視訪問控制會導致數據的修改變得不可控,從而產生意想不到的缺陷;
- 私有成員可能會隨着開發/更新/修復/重構等活動而改變,過度依賴於訪問其他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,當然這也是不得以才為之的辦法,那么對於即想嘗試新版本又不想被警告信息煩擾的話該怎么辦呢,只能嘗試下面幾種辦法了:
- 如果是第三方依賴導致的警告,升級對應的依賴或是聯系該依賴的維護人員,請他們盡快修復問題。通常來說這類向后兼容的問題修復無需花費很多的時間。
- 如果是自己的代碼,則考慮是否要對私有成員提供可訪問的public API,是否將私有成員公有化。
- 如果使用了某些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
