Android避坑指南,Gson与Kotlin碰撞出一个不安全的操作

本文讲述了在Kotlin中使用DataClass与Gson进行数据转换时,遇到的关于空安全和构造器调用的意外问题。作者揭示了Gson如何在不调用父类构造方法的情况下创建对象,以及为何这种情况罕见但可能发生。
摘要由CSDN通过智能技术生成

好了,已经超出我的知识点范畴了。

不免心中嘀咕:

这题目太偏了,毫无意义,而且文章标题是 Android 避坑指南,看起来毫无关系

是的,确实很偏,跳过这个问题,我们往下看,看看是怎么在Android开发过程中遇到的,而且看完后,这个问题就迎刃而解了。

问题的来源


上周一个群有个小伙伴,遇到了一个Kotlin写的Bean,在做Gson将字符串转化成具体的Bean对象时,发生了一个不符合预期的问题。

因为是他们项目的代码,我就不贴了,我写了个类似的小例子来替代。

对于Java Bean,kotlin可以用data class,网上也有很多博客表示:

在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass,使用 DataClass 编译器会默默地帮我们生成一些函数。

我们先写个Bean:

data class Person(var name: String, var age: Int) {

}

这个Bean是用于接收服务器数据,通过Gson转化为对象的。

简化一下代码为:

val gson = Gson()

val person = gson.fromJson(“{“age”:“12”}”, Person::class.java)

我们传递了一个json字符串,但是没有包含key为name的值,并且注意:

在Person中name的类型是String,也就是说是不允许name=null的

那么上面的代码,我运行起来结果是什么呢?

  1. 报错,毕竟没有传name的值;

  2. 不报错,name 默认值为"";

  3. 不报错,name=null;

感觉1最合理,也符合Kotlin的空安全检查。

验证一下,修改一下代码,看一下输出:

val gson = Gson()

val person = gson.fromJson(“{“age”:“12”}”, Person::class.java)

println(person.name )

输出结果:

null

是不是有些奇怪,感觉意外绕过了Kotlin的空类型检查。

所以那位出问题的同学,在这里之后数据就出了问题,导致一直排查困难。

我们再改一下代码:

data class Person(var name: String, var age: Int): People(){

}

我们让Person继承自People类:

public class People {

public People(){

System.out.println(“people cons”);

}

}

在People类的构造方法中打印日志。

我们都清楚,正常情况下,一般构造子类对象,必然会先执行父类的构造方法。

运行一下:

没有执行父类构造方法,但对象构造出来了

这里可以猜到,Person对象的构建,并不是常规的构建对象,没有走构造方法。

那么它是怎么做到的呢?

那只能去Gson的源码中取找答案了。

找到其怎么做的,其实就相当于解答了我们文首的问题。

追查原因


Gson这样构造出一个对象,但是没有走父类构造这种,如果真是的这样,那么是极其危险的。

会让程序完全不符合运行预期,少了一些必要逻辑。

所以我们提前说一下,大家不用太惊慌,并不是Gson很容易出现这样的情况,而是恰好上例的写法碰上了,我们一会会说清楚。

首先我们把Person这个kotlin的类,转成Java,避免背后藏了一些东西:

反编译之后的显示

public final class Person extends People {

@NotNull

private String name;

private int age;

@NotNull

public final String getName() {

return this.name;

}

public final void setName(@NotNull String var1) {

Intrinsics.checkParameterIsNotNull(var1, “<set-?>”);

this.name = var1;

}

public final int getAge() {

return this.age;

}

public final void setAge(int var1) {

this.age = var1;

}

public Person(@NotNull String name, int age) {

Intrinsics.checkParameterIsNotNull(name, “name”);

super();

this.name = name;

this.age = age;

}

// 省略了一些方法。

}

可以看到Person有一个包含两参的构造方法,并且这个构造方法中有name的空安全检查。

也就是说,正常通过这个构造方法构建一个Person对象,是不会出现空安全问题的。

那么只能去看看Gson的源码了:

Gson的逻辑,一般都是根据读取到的类型,然后找对应的TypeAdapter去处理,本例为Person对象,所以会最终走到ReflectiveTypeAdapterFactory.create然后返回一个TypeAdapter。

我们看一眼其内部代码:

ReflectiveTypeAdapterFactory.create

@Override

public TypeAdapter create(Gson gson, final TypeToken type) {

Class<? super T> raw = type.getRawType();

if (!Object.class.isAssignableFrom(raw)) {

return null; // it’s a primitive!

}

ObjectConstructor constructor = constructorConstructor.get(type);

return new Adapter(constructor, getBoundFields(gson, type, raw));

}

重点看constructor这个对象的赋值,它一眼就知道跟构造对象相关。

ConstructorConstructor.get

public ObjectConstructor get(TypeToken typeToken) {

final Type type = typeToken.getType();

final Class<? super T> rawType = typeToken.getRawType();

// …省略一些缓存容器相关代码

ObjectConstructor defaultConstructor = newDefaultConstructor(rawType);

if (defaultConstructor != null) {

return defaultConstructor;

}

ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(type, rawType);

if (defaultImplementation != null) {

return defaultImplementation;

}

// finally try unsafe

return newUnsafeAllocator(type, rawType);

}

可以看到该方法的返回值有3个流程:

  1. newDefaultConstructor

  2. newDefaultImplementationConstructor

  3. newUnsafeAllocator

我们先看第一个newDefaultConstructor

private ObjectConstructor newDefaultConstructor(Class<? super T> rawType) {

try {

final Constructor<? super T> constructor = rawType.getDeclaredConstructor();

if (!constructor.isAccessible()) {

constructor.setAccessible(true);

}

return new ObjectConstructor() {

@SuppressWarnings(“unchecked”) // T is the same raw type as is requested

@Override public T construct() {

Object[] args = null;

return (T) constructor.newInstance(args);

// 省略了一些异常处理

};

} catch (NoSuchMethodException e) {

return null;

}

}

可以看到,很简单,尝试获取了无参的构造函数,如果能够找到,则通过newInstance反射的方式构建对象。

追随到我们的Person的代码,其实该类中只有一个两参的构造函数,并没有无参构造,从而会命中NoSuchMethodException,返回null。

返回null会走newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。

那么,最后只能走:**newUnsafeAllocator ** 方法了。

从命名上面就能看出来,这是个不安全的操作。

newUnsafeAllocator最终是怎么不安全的构建出一个对象呢?

往下看,最终执行的是:

public static UnsafeAllocator create() {

// try JVM

// public class Unsafe {

// public Object allocateInstance(Class<?> type);

// }

try {

Class<?> unsafeClass = Class.forName(“sun.misc.Unsafe”);

Field f = unsafeClass.getDeclaredField(“theUnsafe”);

f.setAccessible(true);

final Object unsafe = f.get(null);

final Method allocateInstance = unsafeClass.getMethod(“allocateInstance”, Class.class);

return new UnsafeAllocator() {

@Override

@SuppressWarnings(“unchecked”)

public T newInstance(Class c) throws Exception {

assertInstantiable©;

return (T) allocateInstance.invoke(unsafe, c);

}

};

} catch (Exception ignored) {

}

// try dalvikvm, post-gingerbread use ObjectStreamClass

本人从事网路安全工作12年,曾在2个大厂工作过,安全服务、售后服务、售前、攻防比赛、安全讲师、销售经理等职位都做过,对这个行业了解比较全面。

最近遍览了各种网络安全类的文章,内容参差不齐,其中不伐有大佬倾力教学,也有各种不良机构浑水摸鱼,在收到几条私信,发现大家对一套完整的系统的网络安全从学习路线到学习资料,甚至是工具有着不小的需求。

最后,我将这部分内容融会贯通成了一套282G的网络安全资料包,所有类目条理清晰,知识点层层递进,需要的小伙伴可以点击下方小卡片领取哦!下面就开始进入正题,如何从一个萌新一步一步进入网络安全行业。

学习路线图

其中最为瞩目也是最为基础的就是网络安全学习路线图,这里我给大家分享一份打磨了3个月,已经更新到4.0版本的网络安全学习路线图。

相比起繁琐的文字,还是生动的视频教程更加适合零基础的同学们学习,这里也是整理了一份与上述学习路线一一对应的网络安全视频教程。

网络安全工具箱

当然,当你入门之后,仅仅是视频教程已经不能满足你的需求了,你肯定需要学习各种工具的使用以及大量的实战项目,这里也分享一份我自己整理的网络安全入门工具以及使用教程和实战。

项目实战

最后就是项目实战,这里带来的是SRC资料&HW资料,毕竟实战是检验真理的唯一标准嘛~

面试题

归根结底,我们的最终目的都是为了就业,所以这份结合了多位朋友的亲身经验打磨的面试题合集你绝对不能错过!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值