Gson与Kotlin的老生常谈的空安全问题

1,346 阅读4分钟

问题出现

偶然在一次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); 我手动尝试了一下可以创建出对应的实例,而且和通常的构造函数创建出来的实例有所区别

image.png 很明显,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解析库即可。