故事從一個Koltin
專案新功能的除錯過程說起,應用丟擲了一個異常,堆疊資訊(區域性)如下:
[http-nio-8080-exec-2] ERROR c.c.p.f.i.c.c.e.GlobalExceptionHandler - java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ${className}.<init>, parameter ${fieldName}
複製程式碼
是一個常見的空安全異常,向空安全的變數賦了Null
值。但值變數是也是來自空安全變數,為什麼會出現這樣的情況呢?接下來逐步分析。
Koltin 的空安全
Kotlin
的在類成員變數的空安全是在編譯級別實現的,即在編譯成class
檔案的時候在類的建構函式新增了空值檢查
// 第一個引數是建構函式傳入的變數值,第二個引數是變數名
Intrinsics.checkParameterIsNotNull(name, "name");
複製程式碼
這個檢查方法會在傳入Null
值時丟擲java.lang.IllegalArgumentException
異常,以此保證類的成員屬性是空安全。
Gson的反序列化
Gson
的反序列化的主流程邏輯集中在ReflectiveTypeAdapterFactory.BoundField.read()
方法中
@Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
複製程式碼
流程十分簡單:
- 檢查
Json
的輸入流,為空則返回Null
- 例項化泛型引數型別的物件例項
instance
- 解析
Json
字串給instance
屬性賦值
最重要的是第二步,即物件的例項化過程constructor.construct()
。constructor
是一個什麼物件呢?
constructor
是一個封裝了物件構造方法的物件,它的生成邏輯如下:
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();
// first try an instance creator
@SuppressWarnings("unchecked") // types must agree
final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
if (typeCreator != null) {
return new ObjectConstructor<T>() {
@Override public T construct() {
return typeCreator.createInstance(type);
}
};
}
// Next try raw type match for instance creators
@SuppressWarnings("unchecked") // types must agree
final InstanceCreator<T> rawTypeCreator =
(InstanceCreator<T>) instanceCreators.get(rawType);
if (rawTypeCreator != null) {
return new ObjectConstructor<T>() {
@Override public T construct() {
return rawTypeCreator.createInstance(type);
}
};
}
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(type, rawType);
}
複製程式碼
- 如果註冊過
InstanceCreator
,則返回註冊的InstanceCreator
- 如果類有無參建構函式,則返回撥用無參建構函式的
InstanceCreator
- 如果是集合類,則返回 newDefaultImplementationConstructor生成的
InstanceCreator
- 否則交給
UnsafeAllocator
這次異常的分支條件呼叫的是UnsafeAllocator
,其原始碼就不進行具體解析,其工作原理就是包裝了sun.misc.Unsafe
的方法來完成物件的例項化,這個sun.misc.Unsafe
就是這次異常的“病根”。
類的例項化 & sun.misc.Unsafe
在Java/Kotlin
中,物件的建立方式比較常見的是以下幾種:
new
語句, 比如MyClass demo = new MyClass()
Class
物件的newInstance()
方法,比如MyClass demo = MyClass.class.newInstance()
(前提就是必須提供無參的建構函式)- 利用
Constructor
物件來建立物件
方式雖多,但殊途同歸,在JVM層面,其對應都是三條重要指令,以java.lang.StringBuilder
為例
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
複製程式碼
其對應的邏輯分表別是
- 分配物件所需記憶體並返回記憶體地址壓入棧頂
- 複製一份上述記憶體地址並在壓入棧頂
- 執行建立物件的
<init>
方法
可以看到物件的建立過程不是原子性的,所以還存在一種特殊途徑來建立物件,即sun.misc.Unsafe
類。
它建立物件的方法和放射類似,但它不會執行INVOKESPECIAL
指令,建立的物件的所有成員變數都處於空值狀態。
那麼回到應用的異常情況,在執行時Gson
通過反序列化生成了一個與型別宣告不符的“半成品”物件,這個異常值的傳遞至一個空安全欄位時,就受到了Koltin
的檢查導致異常發生。
總結
Kotlin
的空安全確實給開發人員帶了極大的便利,但也帶來了隱患,即可能會給開發人員帶來虛假的安全感:Kotlin
終究還是在基於JVM
的靜態語言,面對類似sun.misc.Unsafe
等類似的底層操作,空檢查可以被輕易突破,且這種非法物件的存在是渾然不知的,但是卻給我們的應用帶來真切的Bug
,在面對空安全屬性上,各位同學還是需要多留個心眼,防範這些“非法移民”。