故事从一个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
,在面对空安全属性上,各位同学还是需要多留个心眼,防范这些“非法移民”。