Kotlin的空安全真的安全吗?

1,901 阅读4分钟

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