|
| 1 | +# Kotlin 遇到 MyBatis:到底是 Int 的错,还是 data class 的错? |
| 2 | + |
| 3 | +[toc] |
| 4 | + |
| 5 | +## 问题出现 |
| 6 | + |
| 7 | +前不久刚刚应小伙伴的要求拉了个 QQ 群:162452394 (微信群可以加我微信 enbandari,邀大家进群),上周一的时候在公众号推送了之后一下子就热闹起来了。 |
| 8 | + |
| 9 | +<img src="../../arts/e_group.png" width="250px"/> |
| 10 | + |
| 11 | + |
| 12 | +话说有个哥们在群里面问了这么一个问题,他用 MyBatis 来接入数据库,有个实体类用 Kotlin 大概是这么写的: |
| 13 | + |
| 14 | +```kotlin |
| 15 | +data class User (var id: Int, var username: String, var age: Int, var passwd: String) |
| 16 | +``` |
| 17 | + |
| 18 | +它对应的数据库表是这样的: |
| 19 | + |
| 20 | +```sql |
| 21 | +CREATE TABLE userinfo |
| 22 | +( |
| 23 | + id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, |
| 24 | + username VARCHAR(45), |
| 25 | + age INT(11), |
| 26 | + passwd VARCHAR(45) |
| 27 | +); |
| 28 | +``` |
| 29 | + |
| 30 | +字段顺序也都能对得上。 |
| 31 | + |
| 32 | +然后呢,他就配置了这么一条查询语句: |
| 33 | + |
| 34 | +```xml |
| 35 | +<mapper namespace="net.println.kotlin.mybatis.UserMapper"> |
| 36 | + <select id="selectUser" resultType="net.println.kotlin.mybatis.User"> |
| 37 | + select * from userinfo where id = #{id} |
| 38 | + </select> |
| 39 | +</mapper> |
| 40 | +``` |
| 41 | +对应的 UserMapper 代码如下: |
| 42 | + |
| 43 | +```kotlin |
| 44 | +public interface UserMapper { |
| 45 | + User selectUser(int id); |
| 46 | +} |
| 47 | +``` |
| 48 | +这一切看上去似乎一点儿毛病都没有哇,可一旦他调用 selectUser 方法之后,程序开始抱怨了: |
| 49 | + |
| 50 | +``` |
| 51 | +No constructor found in net.println.kotlin.mybatis.User matching [java.lang.Integer, java.lang.String, java.lang.Integer, java.lang.String] |
| 52 | +``` |
| 53 | + |
| 54 | +啥问题呢?找不到构造方法。当时看到这个问题的时候正好手里有活,没有仔细看,周末特意照着写了个 demo,果然。。嗯。。居然找不到构造方法,这就有意思了。 |
| 55 | + |
| 56 | +## 问题探究 ① —— Kotlin 的类型映射 |
| 57 | + |
| 58 | +按理说,我们的 data class 是有构造方法的,说找不到构造方法倒也有些不公平,应该确切的说是找不到合适的构造方法。前面那句错误信息告诉我们 MyBatis 想要找的构造方法是下面的签名: |
| 59 | + |
| 60 | +```java |
| 61 | +init(java.lang.Integer, java.lang.String, java.lang.Integer, java.lang.String) |
| 62 | +``` |
| 63 | +我们的 data class 的构造方法呢? |
| 64 | + |
| 65 | +```java |
| 66 | +init(kotlin.Int, kotlin.String, kotlin.Int, kotlin.String) |
| 67 | +``` |
| 68 | +嗯,乍一看确实不一样哈,难怪找不到合适的构造方法。这样说对吗?我在之前有篇文章[为什么不直接使用 Array<Int> 而是 IntArray ?](https://mp.weixin.qq.com/s?__biz=MzIzMTYzOTYzNA==&mid=2247483762&idx=1&sn=c553cdd4b0bccd59e530a9f231037d73&chksm=e8a05e4fdfd7d759a01eef26adae2ddf35f05f253e056490cc62972eda790f9601f23b2a8a23#rd)提到 过 Kotlin 的类型映射的问题,kotlin.String 编译之后毫无疑问的要映射成 java.lang.String,而 kotlin.Int 则有可能映射成 int 或是 java.lang.Integer,这么说来我们的 User 的构造方法签名可能是下面这样: |
| 69 | + |
| 70 | +```java |
| 71 | +init(int, java.lang.String, int, java.lang.String) |
| 72 | +``` |
| 73 | + |
| 74 | +也可能是这样: |
| 75 | + |
| 76 | +```java |
| 77 | +init(java.lang.Integer, java.lang.String, java.lang.Integer, java.lang.String) |
| 78 | +``` |
| 79 | + |
| 80 | +现在通过刚才报的错误来看,映射后的签名毫无疑问的应该是前面那种了,毕竟这里 Int 并没有装箱的需求,为了追求效率,映射成 int 是再合适不过的了。也正是这个原因,MyBatis 才无法找到它想要的构造方法,无法构造出 User 对象,最终导致程序运行失败。 |
| 81 | + |
| 82 | +## 问题探究 ② —— JavaBean 的无参构造 |
| 83 | + |
| 84 | +JavaBean 是一个很有意思的概念,刚刚接触这个概念的时候都有点儿不敢相信自己的耳朵,一个在 JavaEE 当中举足轻重的概念居然就只是一个有无参构造方法、属性通过 Getter 和 Setter 访问、可序列化和反序列化的 POJO,就这么简单?说实在的,当时真觉得 JavaBean 也没什么了不起的,就像最开始学牛二定律的时候一样,一个只有 4 个字符的定律,料它也不能把洒家怎样——可是实际上呢,它确实把我给怎样了。。 |
| 85 | + |
| 86 | +刚刚我们分析错误的时候,很直接的分析了构造方法为什么不匹配的原因,却没有想想为什么要找这个构造方法,试想,如果你用 Java 写这段代码,你肯定会写出类似下面的代码: |
| 87 | + |
| 88 | +```java |
| 89 | +public class User{ |
| 90 | + private int id; |
| 91 | + private String username; |
| 92 | + private int age; |
| 93 | + private String passwd; |
| 94 | + |
| 95 | + ... 省略 getter 和 setter |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +如果不纠结序列化的事儿,这个 User 就是个 JavaBean 是吧,你交给 MyBatis 使用的话也不会出现任何问题—— MyBatis 压根儿不需要找什么构造方法,因为人家根本不需要费那劲,有无参的默认构造方法的话,构造对象实例岂不是轻而易举? |
| 100 | + |
| 101 | +对咯,MyBatis 其实想要的是一个 JavaBean,一个有默认无参构造方法的类,结果呢,你给人家塞了一个 data class 过去。。 |
| 102 | + |
| 103 | +## 解决方案 ① —— 我就用 Integer 了怎么着吧 |
| 104 | + |
| 105 | +这个问题有一个最为直接的解决办法,那就是直接使用 Integer 而不是 kotlin.Int。 |
| 106 | + |
| 107 | +```kotlin |
| 108 | +data class User (var id: Integer, var username: String, var age: Integer, var passwd: String) |
| 109 | +``` |
| 110 | + |
| 111 | +不过,你一旦这么写了,你就没办法在 Kotlin 当中正常实例化这个类了(在 Java 中可以实例化),所以这种方案堪比七伤拳啊: |
| 112 | + |
| 113 | +```kotlin |
| 114 | +val user = User(1,"root", 30,"") //error : The integer literal does not conform to the expected type Integer |
| 115 | +``` |
| 116 | + |
| 117 | +## 解决方案 ② —— kotlin.Int 什么时候映射为 Integer |
| 118 | + |
| 119 | +如果 kotlin.Int 能够映射成 java.lang.Integer,那么这问题就彻底解了。试想一下,什么情况下 int 不好使,非得用 Integer? |
| 120 | + |
| 121 | +* 整型作为泛型参数的时候 |
| 122 | +* 可以为 null |
| 123 | + |
| 124 | +这两种情况显而易见的需要 Integer 出马了,比如你想将一堆整数放入 ArrayList 当中,你只能这么搞: |
| 125 | + |
| 126 | +```java |
| 127 | +ArrayList<Integer> integers = new ArrayList<Integer>(); |
| 128 | +... |
| 129 | +``` |
| 130 | + |
| 131 | +还有一种就是整型值可能为 null 的时候,毕竟作为基本类型的 int 连默认值都是 0,怎么会为 null 呢? |
| 132 | + |
| 133 | +回到我们的问题,如果能让 data class 的 Int 映射为 Integer,那么构造方法应该是妥妥的了: |
| 134 | + |
| 135 | +```kotlin |
| 136 | +data class User (var id: Int?, var username: String, var age: Int?, var passwd: String) |
| 137 | +``` |
| 138 | + |
| 139 | +我们把构造方法当中的 id 和 age 的类型做了修改,从不可为空的 Int 改为可为空的 Int?,这样编译之后就只好映射为 Integer了。 |
| 140 | + |
| 141 | +问题解决~ |
| 142 | + |
| 143 | +这个方案的优点就是几乎没有额外的依赖或者其他什么开销,只是后续编码时,你会总是被迫对 id、age 这几个属性进行是否为空的判断,这样看起来一点儿都不美。 |
| 144 | + |
| 145 | +## 解决方案 ③ —— 默认参数 |
| 146 | + |
| 147 | +其实就像我们前面提到的,如果 User 这个类有个无参构造的话,后面查找其他构造方法的事儿就压根儿不会有。也就是说如果我们给 User 类加一个无参构造,这个问题也是可以解决的: |
| 148 | + |
| 149 | +```kotlin |
| 150 | +class User { |
| 151 | + var id: Int = 0 |
| 152 | + lateinit var username: String |
| 153 | + var age: Int = 0 |
| 154 | + lateinit var passwd: String |
| 155 | +} |
| 156 | +``` |
| 157 | +如果这样写的话,我们就无法享受 data class 带来的书写便利了。。不过如果我们能够骗过 MyBatis 说我们这个类有无参构造,那么问题不就解决了? |
| 158 | + |
| 159 | +```kotlin |
| 160 | +data class User (var id: Int = 0, var username: String = "", var age: Int = 0, var passwd: String = "") |
| 161 | +``` |
| 162 | + |
| 163 | +我们为每一个参数加了默认值, 这样编译出来之后,字节码当中就真的会看到有无参构造方法了: |
| 164 | + |
| 165 | +```kotlin |
| 166 | + public <init>()V |
| 167 | + L0 |
| 168 | + ALOAD 0 |
| 169 | + ICONST_0 |
| 170 | + ACONST_NULL |
| 171 | + ICONST_0 |
| 172 | + ACONST_NULL |
| 173 | + BIPUSH 15 |
| 174 | + ACONST_NULL |
| 175 | + INVOKESPECIAL net/println/kotlin/mybatis/User.<init> (ILjava/lang/String;ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V |
| 176 | + RETURN |
| 177 | + L1 |
| 178 | + MAXSTACK = 7 |
| 179 | + MAXLOCALS = 1 |
| 180 | +``` |
| 181 | + |
| 182 | +实际上我们也可以通过反射来获得到这个无参的构造方法,也正是因为如此,我们也可以直接用 newInstance 方法来构造 User 实例: |
| 183 | + |
| 184 | +```kotlin |
| 185 | +User::class.java.newInstance() |
| 186 | +``` |
| 187 | + |
| 188 | +既然有了无参构造方法,MyBatis 就不需要绞尽脑汁还要找其他的构造方法,于是问题解决~ |
| 189 | + |
| 190 | +这个方案的优点是,比较简单,也没有上一个方案那样的副作用;缺点就是,万一某一个属性没有默认值,你该给它设置什么呢? |
| 191 | + |
| 192 | +## 解决方案 ④ —— 官方也认为有时候我们需要一个无参构造 |
| 193 | + |
| 194 | +早在 1.0.6 发版的时候,官方就增加了对无参构造的一种另类支持,即 noarg 插件。Kotlin 原本不需要这么做,但考虑到它与 Java 解不开理还乱的关系,Java 支持的一切代码的写法 Kotlin 也似乎有责任和义务来完全支持了。 |
| 195 | + |
| 196 | +这个方法其实是 Kotlin 编译插件在编译器通过字节码织入的方式向 class 文件中写入了一个无参构造方法,这个构造方法由于出现的时间比较晚,我们无法在代码中引用到它,不过却可以通过反射访问到它,这样就即保证了 Kotlin 的初心不变,如果你愿意用 data class 或者类似的实体类,那么你就要按照 Kotlin 的要求妥善处理好它的成员的初始化,也方便了一些框架的“出格”行为,显然一个聪明的框架需要对代码本身有足够的理解,对编码人员的限制对于框架本身来说就显得没有那么的重要了。 |
| 197 | + |
| 198 | +如果你遇到了这样的问题,我当然建议你采用官方的这个解决方案,原因很简单,除了要写一个注解之外,几乎没有任何副作用,另外,官方支持的方案自然也比较有保障啦。 |
| 199 | + |
| 200 | +## 拓展延伸 —— 不择手段创建实例 |
| 201 | + |
| 202 | +说起来我就要批评一下 MyBatis 了,一点儿都不如 Gson 流氓。我们前面虽然没有细说,不过大家基本上可以知道 MyBatis 是如何创建返回结果的实例的: |
| 203 | + |
| 204 | +```java |
| 205 | + private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) |
| 206 | + throws SQLException { |
| 207 | + ... |
| 208 | + } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) { |
| 209 | + //有无参构造方法的话走的是这个分支 |
| 210 | + return objectFactory.create(resultType); |
| 211 | + } else if (shouldApplyAutomaticMappings(resultMap, false)) { |
| 212 | + //在这里查找与表结构匹配的构造方法,我们之前遇到的错误就在这个方法当中抛出 |
| 213 | + return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix); |
| 214 | + } |
| 215 | + ... |
| 216 | + } |
| 217 | +``` |
| 218 | + |
| 219 | +我们看到如果没有找到匹配的构造方法,也没有无参的构造方法,MyBatis 就叹了一口气,放弃了。这样的事情如果交给 Gson,你就会发现完全不一样。我曾在[12 Json数据引发的血案](https://mp.weixin.qq.com/s?__biz=MzIzMTYzOTYzNA==&mid=2247483683&idx=1&sn=41ca82c89ddccaf61a9c734e5976a62b&chksm=e8a05e1edfd7d7086b84db34640043054b2288cc5e45bdc300cfab2ce16ffb84c66438a16dca#rd)这一期当中介绍过 Gson 如何创建实例,它甚至可以让 Kotlin 的不可空类型“赋值”为 null,原因很简单,它在实例化对象的时候也跟 MyBatis 一样,先去找无参构造,找不到就用 Unsafe.allocateInstance 来创建对象,主要这个创建方法非常的底层,你可以简单的理解为只为实例化出来的 Java 对象开辟了对象存续需要的空间,而对应地它的成员没有一个会正常初始化。 |
| 220 | + |
| 221 | +```kotlin |
| 222 | +class Test{ |
| 223 | + init { |
| 224 | + println("init") |
| 225 | + } |
| 226 | + |
| 227 | + companion object{ |
| 228 | + init { |
| 229 | + println("cinit") |
| 230 | + } |
| 231 | + } |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +注意到这段代码,cinit 将在 Test 类加载时打印,init 将在 Test 实例化时打印。 |
| 236 | + |
| 237 | +```kotlin |
| 238 | +val field = Unsafe::class.java.getDeclaredField("theUnsafe") |
| 239 | +field.isAccessible = true |
| 240 | +val unsafe = field.get(null) as Unsafe |
| 241 | +unsafe.allocateInstance(Test::class.java) |
| 242 | +``` |
| 243 | + |
| 244 | +我们运行这样的程序,结果只有 cinit,难怪人家叫 Unsafe,都告诉你 Unsafe 了你还想要什么。。 |
| 245 | + |
| 246 | +不过这在 C++ 当中,简直不叫事儿,不信给你看一段代码: |
| 247 | + |
| 248 | +```cpp |
| 249 | +class Hello { |
| 250 | +public: |
| 251 | + int getNum(); |
| 252 | + |
| 253 | + int checkNum(int a, int b = 0); |
| 254 | +}; |
| 255 | +... |
| 256 | +int Hello::getNum() { |
| 257 | + return 12310; |
| 258 | +} |
| 259 | + |
| 260 | +... |
| 261 | +using namespace std; |
| 262 | + |
| 263 | +int main() { |
| 264 | + cout << ((Hello*)0)->getNum() << endl; |
| 265 | + return 0; |
| 266 | +} |
| 267 | +``` |
| 268 | +
|
| 269 | +我们把一个 0 强转为 Hello 类型的指针,接着还调用人家的函数 getNum,结果你猜怎么着?运行结果还是对的! |
| 270 | +
|
| 271 | +如果你经常接触 Jni,你也经常会把 native 的指针传给 Java,Java 拿到的其实就是一个 long 类型的数,等 Java 需要调用 native 代码的时候,你就会发现这个整数传给 native 层会首先被 reinterpret_cast。 |
| 272 | +
|
| 273 | +这有什么稀罕的,反正你创建的类也好,对象也好,最终都是数,严格的语法限制也不过是编译器给我们盖起的围墙,你通过围墙来保护你自己,同时也让围墙遮挡了你的眼睛。 |
| 274 | +
|
| 275 | +<center></center> |
0 commit comments