Skip to content

Commit a9e3973

Browse files
committed
提交27号的文章
1 parent 0a0c664 commit a9e3973

File tree

18 files changed

+698
-0
lines changed

18 files changed

+698
-0
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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>![](../../arts/kotlin扫码关注.png)</center>

code/MyBatisIssue/build.gradle

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
group 'net.println.kotlin'
2+
version '1.0-SNAPSHOT'
3+
4+
buildscript {
5+
ext.kotlin_version = '1.1.0'
6+
7+
repositories {
8+
mavenCentral()
9+
}
10+
dependencies {
11+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12+
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
13+
}
14+
}
15+
16+
apply plugin: 'java'
17+
apply plugin: 'kotlin'
18+
apply plugin: "kotlin-noarg"
19+
20+
noArg{
21+
annotation("net.println.kotlin.mybatis.annotations.PoKo")
22+
}
23+
24+
sourceCompatibility = 1.5
25+
26+
repositories {
27+
mavenCentral()
28+
}
29+
30+
dependencies {
31+
compile fileTree(include: ['*.jar'], dir: 'libs')
32+
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
33+
compile "org.mybatis:mybatis:3.4.2"
34+
testCompile group: 'junit', name: 'junit', version: '4.11'
35+
}
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#Sat Mar 25 09:41:11 CST 2017
2+
distributionBase=GRADLE_USER_HOME
3+
distributionPath=wrapper/dists
4+
zipStoreBase=GRADLE_USER_HOME
5+
zipStorePath=wrapper/dists
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip

0 commit comments

Comments
 (0)