问题出现
偶然在一次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);
我手动尝试了一下能够创立出对应的实例,并且和通常的结构函数创立出来的实例有所区别
很明显,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解析库即可。