注册

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

问题出现


偶然在一次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解析库即可。


作者:Lowae
链接:https://juejin.cn/post/7245682479787589691
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册