问题出现

偶然在一次debug中发现了一个按常理不应出现的NPE,用以下简化示例为例:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.getValue()" because "<local1>" is null

对应的数据模型如下:

class Book(
    val id: Int,  
    val name: String?  
) {  
    val summary by lazy { id.toString() + name }  
}

发生在调用book.summary中。第一眼我是很疑惑了,怎样by lazy也能是null,因为summary自身就是一个委托属性,所以看看summary是怎样初始化的吧,反编译为java可知,在结构函数初始化,这彻底没啥问题。

public final class Book {
   @NotNull
   private final Lazy summary$delegate;
   private final int id;
   @Nullable
   private final String name;
   @NotNull
   public final String getSummary() {
      Lazy var1 = this.summary$delegate;
      Object var3 = null;
      return (String)var1.getValue();
   }
   ...略去其他
   public Book(int id, @Nullable String name) {
      this.id = id;
      this.name = name;
      this.summary$delegate = LazyKt.lazy((Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            return this.invoke();
         }
         @NotNull
         public final String invoke() {
            return Book.this.getId() + Book.this.getName();
         }
      }));
   }
}

所以仅有的可能性就是结构函数并未履行。而这块逻辑是存在json的解析的,而Gson与kotlin的空安全问题老生常谈了,便立马往这个方向排查。

追根溯源

直接找到Gson里的ReflectiveTypeAdapterFactory类,它是用于处理普通 Java 类的序列化和反序列化。作用是根据目标的类型和字段的反射信息,生成相应的 TypeAdapter 目标,以履行序列化和反序列化的操作。 然后再看到create办法,这也是TypeAdapterFactory的笼统办法

  @Override
  public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
    Class<? super T> raw = type.getRawType();
    if (!Object.class.isAssignableFrom(raw)) {
      return null; // it's a primitive!
    }
    FilterResult filterResult =
        ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
    if (filterResult == FilterResult.BLOCK_ALL) {
      throw new JsonIOException(
          "ReflectionAccessFilter does not permit using reflection for " + raw
              + ". Register a TypeAdapter for this type or adjust the access filter.");
    }
    boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
    // If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
    // on JVMs that do not support records.
    if (ReflectionHelper.isRecord(raw)) {
      @SuppressWarnings("unchecked")
      TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
          getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
      return adapter;
    }
    ObjectConstructor<T> constructor = constructorConstructor.get(type);
    return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
  }

最终到了ObjectConstructor<T> constructor = constructorConstructor.get(type);这一句,这很明显是一个类的结构器,继续走到里边的get办法

  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();
    // ...省掉其他部分逻辑
    // First consider special constructors before checking for no-args constructors
    // below to avoid matching internal no-args constructors which might be added in
    // future JDK versions
    ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
    if (specialConstructor != null) {
      return specialConstructor;
    }
    FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }
    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }
    ...
    // Consider usage of Unsafe as reflection,
    return newUnsafeAllocator(rawType);
  }

先来看看前三个Constructor,

  • newSpecialCollectionConstructor
    • 注释说是供给给特殊的无参的集合类结构函数创立的结构器,里边的也只是判断了是否为EnumSet和EnumMap,未匹配上,跳过
  • newDefaultConstructor
    • 里边直接调用的Class.getDeclaredConstructor(),运用默许结构函数创立,很明显看最上面的结构是无法创立的,抛出NoSuchMethodException
  • newDefaultImplementationConstructor
    • 里边都是集合类的创立,如Collect和Map,也不是

最终,只能走到了newUnsafeAllocator()

  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
    if (useJdkUnsafe) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          try {
            @SuppressWarnings("unchecked")
            T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
            return newInstance;
          } catch (Exception e) {
            throw new RuntimeException(("Unable to create instance of " + rawType + ". "
                + "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args "
                + "constructor may fix this problem."), e);
          }
        }
      };
    } else {
      final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe "
          + "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args "
          + "constructor, or enabling usage of JDK Unsafe may fix this problem.";
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          throw new JsonIOException(exceptionMessage);
        }
      };
    }
  }

缘由揭晓

办法内部调用了UnsafeAllocator.INSTANCE.newInstance(rawType); 我手动尝试了一下能够创立出对应的实例,并且和通常的结构函数创立出来的实例有所区别

Gson与Kotlin的老生常谈的空安全问题
很明显,summary的委托属性是null的,阐明该办法是不走结构函数来创立的,里边的实现是经过Unsafe类的allocateInstance来直接创立对应ClassName的实例。

解决计划

看到这便现已知道缘由了,那如何解决这个问题?

计划一

回到上面的Book反编译后的java代码,能够看到只要调用了结构函数即可,所以增加一个默许的无参结构函数就是一个可行的计划。改动如下:

class Book(
    val id: Int = 0,
    val name: String? = null
) {
    val summary by lazy { id.toString() + name }
}

或许手动加一个无参结构函数

class Book(
    val id: Int,
    val name: String?
) {
    constructor() : this(0, null)
    val summary by lazy { id.toString() + name }
}

并且要特别注意一定要供给默许的无参结构函数,不然经过newUnsafeAllocator创立的实例就导致kotlin的空安全机制就彻底失效了

计划二

用moshi吧,用一个对kotlin支撑比较好的json解析库即可。