类型安全

最近大家在解决sonar bug,我也看了一下,相当一部分sonar bug都是因为使用了类型不安全的库导致的。java本身是类型安全的,以java所写的代码一般也以类型安全为优美,但是我发现我们同学写的代码中有非常多的类型不安全的实现,特写一篇文章宣讲记录。

注:类型安全的定义在不同地方会有不同,本文指的是相当宽泛的类型安全。

大家都知道fastjson频繁爆出bug,而这些bug中最严重的一些,又是因为AutoType这一特性。这个事情说来话长,本文不展开,这里提供一篇文章供大家参考。AutoType这一特性,就是一种典型的类型不安全的实现,使得本应由开发者控制的诸多关于类型的信息,交由外部字符串控制,从而造成大量问题。sonar检查出的很多问题同属此类,比如对freemaker的限制,等等。

从这个角度来说,反射这一特性,本身就是类型不安全的。使用反射所写出的代码,充满了对Object的强制类型转换,也绕过了编译器的一些限制。假如未来所反射的类升级了,函数签名变化,字段名变化,字段类型变化等,都会对反射代码造成致命影响,造成程序运行期间的重大bug。我个人是很喜欢反射这一特性的,实际编码中也会经常使用,但确实应当慎用。用的好的话,反射很强大,用的差的话,带来的就是难以发现且难以调试的bug。

说了这些,到底类型不安全的代码不安全在哪里?通常的解释是类型安全是内存安全的等价描述,但是这种描述并没有什么意义,反而让人更加云里雾里。这里详细的解释一下内存安全指的是什么。我们从C++开始讲起,假定有一个对象a的类型是class A,A又有几个字段f1,f2,有几个函数m1,m2。这时,对象在内存中如何存储呢?C++的对象是按照虚函数表指针,父类变量,子类变量的顺序存储在内存中的。实际在编译后的代码中,对所有字段和函数的访问,都是用对象地址+偏移的方式来访问的。比如访问字段f1,编译后的代码可能就是a+16(a代表A的内存地址),而访问函数m1,编译后的代码可能是a+8。现在你拿到这个对象a,去访问它的字段,调用它的函数,这都是没有问题的。那内存安全是什么呢?比如你把B类的一个对象b不经过检查强行赋值给了a(在c++中这很容易,在java会困难一些,但通过反射这也根本不是事),或者把a强转成了B(在C++中同样容易,在java中只要使用Object作为强转的中间类型也很容易,后面会举例),这两种方式,都可以达到内存中存的是A类的对象,但是程序以及把它理解成了B类对象,此时,假如访问B类的某个函数m3,它的地址可能是a+16,但是,这实际上是a对象的f1所在的地址,一般情况程序会直接crash,因为根本没法执行。但假如A的f1字段是一个字符串,那么攻击者就可以往f1中存储它想执行的命令的字节码,达到获取系统权限的目的。不要小看内存安全,相当相当多的大型攻击都是通过这种方式完成的。

我也不知道我说清楚没,没说清楚的话可以评论。

上面主要是以C++举例,在java中,这种事情发生的概率会低很多,但是也能做到。比如,我们同学经常使用泛型类型的原始类型,比如Class, List,就会很容易犯下这种错误。比如

List<Integer>  l;
List<String> s;
l = s;//这句会直接报错
List l2 = new List<Integer>;
LIst s2 = new List<String>;
ls = s2;//这句实际上编译是不会报错的

对于反射这种预期的绕过编译检查的,我们不多说啥。但是上述的这种情况,是完全可以避免的。相同的还有:

Integer i;
String s;
i = s; //报错
Object i = 10L;
Object s = "12";
i = s;//同样,编译不会报错

这么蠢的代码我们当然写不出来,但是实际上我们的代码中有非常多代码的跟这些代码差不多,比如用fastjson反序列化出来一个Object,然后直接转成别的类型使用等等,我就不点名了。实际上这些类型不安全的用法,idea是会显示warning的,也就是底色标黄,但是同学们太不在意了。所以写这篇文章,希望大家重视类型安全。

从本质上讲,类型安全是把运行时错误提前到编译错误的一种手段,手段的代价是无法写成灵活性高的代码。但既然选择了java,大部分人都是接受这种代价的。

同时,说个题外话。由于mybatis这个库在很多地方都是类型不安全的,我强烈推荐使用jooq来替换mybatis。mybatis的优势在于用xml编写sql可以最大化性能,而追求性能本来就是和快速开发相悖的。国内外大量创业公司和小型公司(其实也包括很多大型公司的小型项目)都已经使用flyway+jooq作为快速开发的标配,在这里,我也强烈推荐这个组合。

发表评论

电子邮件地址不会被公开。 必填项已用*标注