肝一波 ~ 手写一个简易版的Mybatis,带你深入领略它的魅力!
零、準備工作
<dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.20</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.5</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>provided</scope></dependency> </dependencies>一、JDBC的復雜
1、概述
惡心的一批,缺點賊多
-
我就是為了執行一個SQL,結果需要寫一堆亂七八糟的垃圾玩意,比如?Class.forName、DriverManager.getConnection、connection.createStatement?等,惡心不?
-
執行完SQL,我們需要resultSet.getXxx(int num)來手動封裝到我們的entity對象里,惡心不?
-
SQL直接強耦合到業務代碼里,修改和閱讀都極其惡心。
2、代碼
來一段JDBC代碼看看。
package?com.chentongwei.study.jdbc;import?com.chentongwei.study.entity.User;import?java.sql.*; import?java.util.ArrayList; import?java.util.List;/***?真~~惡心!!!*/ public?class?JdbcDemo?{public?static?void?main(?String[]?args?)?{try?{Class.forName("com.mysql.cj.jdbc.Driver");}?catch?(ClassNotFoundException?e)?{e.printStackTrace();}Connection?connection?=?null;Statement?statement?=?null;ResultSet?resultSet?=?null;try?{connection?=?DriverManager.getConnection("xxx");statement?=?connection.createStatement();//?只有這一句是重點,其他都是垃圾!!!//?只有這一句是重點,其他都是垃圾!!!//?只有這一句是重點,其他都是垃圾!!!resultSet?=?statement.executeQuery("SELECT?*?FROM?user");List<User>?userList?=?new?ArrayList<>();while?(resultSet.next())?{int?id?=?resultSet.getInt(1);String?name?=?resultSet.getString(2);int?age?=?resultSet.getInt(3);userList.add(new?User(id,?name,?age));}}?catch?(SQLException?e)?{e.printStackTrace();}?finally?{if?(null?!=?resultSet)?{try?{resultSet.close();}?catch?(SQLException?e)?{e.printStackTrace();}}if?(null?!=?statement)?{try?{statement.close();}?catch?(SQLException?e)?{e.printStackTrace();}}if?(null?!=?connection)?{try?{connection.close();}?catch?(SQLException?e)?{e.printStackTrace();}}}} } /***?Description:*?<p>*?Project?mybatis-source-study**?@author?TongWei.Chen?2020-06-06?17:12:07*/ @Data @NoArgsConstructor @AllArgsConstructor public?class?User?{private?Integer?id;private?String?name;private?Integer?age; }二、Mybatis的威力
1、概述
他是一個半ORM的框架,為什么是半?因為它支持你直接用它封裝好的selectOne等這些玩意,它也支持手寫SQL,比Hibernate的絕大優勢就是上手簡單、半ORM,沒錯,這種半ORM卻成為了它的優點之一。
這樣我們手寫的SQL想怎么優化就怎么優化,不香嗎?
mybatis優勢(其實也是大多數ORM框架的優勢)
-
你寫你的SQL就完事了,什么?Class.forName?等垃圾代碼都沒了,但是會額外增加其他幾段代碼,但是如果你用了Spring-Mybatis的話那你直接寫你的SQL就完事了,沒其他花里胡哨的東西,都給你封裝了。
-
沒有?resultSet.getXxx(int num)?這種惡心的代碼,他自動給我們映射了,可以猜測到他內部有組件為我們將返回的ResultSet封裝到了對應的entity里。
-
SQL寫到mapper或者接口的方法注解上,不會摻雜到業務代碼里。
2、手寫一個Mybatis
2.1說明
為了更好的表達Mybatis的底層原理,這里手寫一個簡易版的mybatis來證明它的核心源碼。這里只演示注解式的(比如@Select),不寫mapper文件了。
2.2、思路
-
得有個interface(也就是Mapper/DAO接口層)
-
jdk動態代理為interface產生具體實現
-
具體實現里肯定要獲取@Select注解里的SQL
-
然后獲取方法參數值
-
SQL里的參數都是?#{xxx}?格式,所以我們要有解析方法參數的方法,比如找到?#{?和?}?的位置,然后把這段內容替換成具體的參數值
-
得到完整的SQL(拼好參數值的)
-
執行SQL
-
解析結果集到entity上
2.3、實現
2.3.1、interface
package?com.chentongwei.mybatis;import?com.chentongwei.study.entity.User; import?org.apache.ibatis.annotations.Param; import?org.apache.ibatis.annotations.Select;import?java.util.List;/***?Description:*?<p>*?Project?mybatis-source-study**?@author?TongWei.Chen?2020-06-06?17:32:52*/ public?interface?UserMapper?{@Select("SELECT?*?FROM?user?WHERE?id?=?#{id}?AND?name?=?#{name}")List<User>?listUser(@Param("id")?Integer?id,?@Param("name")?String?name); }2.3.2、jdk動態代理
public?static?void?main(String[]?args)?{//?jdk動態代理,代理UserMapper接口UserMapper?userMapper?=?(UserMapper)?Proxy.newProxyInstance(MybatisDemo.class.getClassLoader(),?new?Class[]{UserMapper.class},?new?InvocationHandler()?{@Overridepublic?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{//?獲取@Select注解,Select?annotation?=?method.getAnnotation(Select.class);//?獲取參數,以key-value的形式放到map里,比如map.put("id",?1);?map.put("name",?"test");Map<String,?Object>?argsMap?=?buildMethodArgsMap(method,?args);if?(null?!=?annotation)?{//?獲取SQL:SELECT * FROM user WHERE id =?#{id} AND name =?#{name}String[]?values?=?annotation.value();//?1個select注解只能有一個sql,所以直-接values[0]String?sql?=?values[0];//?sql:?SELECT?*?FROM?user?WHERE?id?=?#{id}?AND?name?=?#{name}System.out.println("sql:?"? ?sql);//?將SQL的#{xxx}部分替換成真實的value得到完整的SQL語句sql?=?parseSQL(sql,?argsMap);System.out.println("parseSQL:?"? ?sql);//?如下部分省略了,SQL都得到了,下面就jdbc執行,封裝就完事了。//?jdbc執行//?ResultSet得到結果集反射到entity里,反射有方法可以得到返回值類型和返回值泛型的,比如List、泛型是User?}return?null;}});userMapper.listUser(1,?"test"); }這個方法是描述了所有流程:
1.動態代理UserMapper接口
2.代理類執行listUser方法,參數是1,test
3.獲取listUser方法上的@Select注解
4.獲取@Select注解上的值,也就是SQL語句
5.獲取listUser方法的兩個參數值,1和test,且存到map里,格式是
?Map<String,?Object>?argsMap?=?new?HashMap<>();argsMap.put("id",?1);argsMap.put("name",?"test");6.將SQL的#{xxx}部分替換成真實的value得到完整的SQL語句
SELECT?*?FROM?user?WHERE?id?=?1?AND?name?=?test`7.jdbc執行SQL
8.ResultSet得到結果集反射到entity里
2.3.3、buildMethodArgsMap
public?static?Map<String,?Object>?buildMethodArgsMap(Method?method,?Object[]?args)?{//?最終參數-參數值都放到這里Map<String,?Object>?argsMap?=?new?HashMap<>();//?獲取listUser的所有參數Parameter[]?parameters?=?method.getParameters();if?(parameters.length?!=?args.length)?{throw?new?RuntimeException("參數個數不一致呀,兄弟");}//?別問我為什么這么寫,因為java8的foreach語法要求內部用外部的變量必須final類型,final就沒法 ?操作,所以用數組來玩騷套路int[]?index?=?{0};Arrays.asList(parameters).forEach(parameter?->?{//?獲取每一個參數的@Param注解,里面的值就是參數keyParam?paramAnno?=?parameter.getAnnotation(Param.class);//?獲取參數值:id和nameString?name?=?paramAnno.value();System.out.println(name);//?將參數值放到最終的map里。id:1、name:testargsMap.put(name,?args[index[0]]);index[0]? ?;});return?argsMap; }最終目的就是返回參數map。
1.獲取listUser方法的所有參數
2.獲取每個參數的@Param注解的值,這個值就是map里的key
3.獲取傳進來的args[i]作為value
4.將key-value放到map
2.3.4、parseSQL
/*** sql:SELECT * FROM user WHERE id =?#{id} AND name =?#{name}* argsMap:Map<String,?Object>?argsMap?=?new?HashMap<>();argsMap.put("id",?1);argsMap.put("name",?"test");*/ public?static?String?parseSQL(String?sql,?Map<String,?Object>?argsMap)?{StringBuilder?sqlBuilder?=?new?StringBuilder();//?遍歷sql的每一個字母,判斷是不是#開頭,是的話找到#{,然后請求parseSQLArg方法填充參數值(1,test)for?(int?i?=?0;?i?<?sql.length();?i ?)?{char?c?=?sql.charAt(i);if?(c?==?'#')?{//?找到#的下一個位置,判斷是不是{int?nextIndex?=?i? ?1;char?nextChar?=?sql.charAt(nextIndex);//?如果#后面不是{,則語法報錯if?(nextChar?!=?'{')?{throw?new?RuntimeException(String.format("這里應該是#{\nsql:%s\nindex:%d",?sqlBuilder.toString(),?nextIndex));}StringBuilder?argsStringBuilder?=?new?StringBuilder();//?將#{xxx}換成具體的參數值,找到}的位置,且將xxx放到argsStringBuilder里i?=?parseSQLArg(argsStringBuilder,?sql,?nextIndex);String?argName?=?argsStringBuilder.toString();//?獲取xxx對應的value,填充到SQL里。Object?argValue?=?argsMap.get(argName);if?(null?==?argValue)?{throw?new?RuntimeException(String.format("找不到參數值:%s",?argName));}//?將參數值放到SQL對應的#{xxx}里sqlBuilder.append(argValue.toString());continue;}sqlBuilder.append(c);}return?sqlBuilder.toString(); }主要就干了下面這件事:
將SELECT * FROM user WHERE id = #{id} AND name = #{name}換成
SELECT * FROM user WHERE id = 1 AND name = test
但是需要下面的parseSQLArg來進行解析參數,找到#{xxx}中}的位置。
2.3.5、parseSQLArg
/*** argsStringBuilder:放的是key值,比如"id"、"name"* sql:SELECT * FROM user WHERE id =?#{id} AND name =?#{name}* nextIndex:目前位置是"#{"這個位置。*/ private?static?int?parseSQLArg(StringBuilder?argsStringBuilder,?String?sql,?int?nextIndex)?{//?為啥 ?一次,因為現在nextIndex指向的是{,所以要 1找到{的下一位nextIndex? ?;//?逐個解析SQL的每個字母,判斷是不是"}"for?(;?nextIndex?<?sql.length();?nextIndex? ?)?{char?c?=?sql.charAt(nextIndex);//?如果不是},那么放到argsStringBuilder里,argsStringBuilder放的是key值,比如"id"、"name"if?(c?!=?'}')?{argsStringBuilder.append(c);continue;}//?如果找到了}的位置,則代表argsStringBuilder里已經有完整的key了,比如id或者name。因為}是在key后面的。則返回}的位置if?(c?==?'}')?{return?nextIndex;}}//?如果都沒找到"}",那明顯語法錯誤,因為這個方法的調用者是有“#{”開頭的,然后你這里沒結束“}”,exception就完事了throw?new?RuntimeException(String.format("語法不對,缺少右括號('{')\nindex:%d",?nextIndex)); }找到參數key值放到argsStringBuilder里且找到}的位置inextIndex并返回。
解析SQL里的每個char字母,不是}的話就放到argsStringBuilder里,比如現在位置是{,那么nextIndex ?就是id的i,然后append到argsStringBuilder里,continue,在for,這時候id的d,再append到argsStringBuilder里,以此類推,找到}后就return位置。
2.3.6、完整代碼
package?com.chentongwei.mybatis;import?org.apache.ibatis.annotations.Param; import?org.apache.ibatis.annotations.Select;import?java.lang.reflect.InvocationHandler; import?java.lang.reflect.Method; import?java.lang.reflect.Parameter; import?java.lang.reflect.Proxy; import?java.util.Arrays; import?java.util.HashMap; import?java.util.Map;/***?Description:*?<p>*?Project?mybatis-source-study**?@author?TongWei.Chen?2020-06-06?17:33:01*/ public?class?MybatisDemo?{public?static?void?main(String[]?args)?{UserMapper?userMapper?=?(UserMapper)?Proxy.newProxyInstance(MybatisDemo.class.getClassLoader(),?new?Class[]{UserMapper.class},?new?InvocationHandler()?{@Overridepublic?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{System.out.println("代理類生效了,方法名:"? ?method.getName()? ?", 參數是:"? ?Arrays.toString(args));Select?annotation?=?method.getAnnotation(Select.class);Map<String,?Object>?argsMap?=?buildMethodArgsMap(method,?args);if?(null?!=?annotation)?{String[]?values?=?annotation.value();//?1個select注解只能有一個sql,所以直接values[0]String?sql?=?values[0];System.out.println("sql:?"? ?sql);sql?=?parseSQL(sql,?argsMap);System.out.println("parseSQL:?"? ?sql);}return?null;}});userMapper.listUser(1,?"test");}public?static?String?parseSQL(String?sql,?Map<String,?Object>?argsMap)?{StringBuilder?sqlBuilder?=?new?StringBuilder();for?(int?i?=?0;?i?<?sql.length();?i ?)?{char?c?=?sql.charAt(i);if?(c?==?'#')?{//?找到#的下一個位置,判斷是不是{int?nextIndex?=?i? ?1;char?nextChar?=?sql.charAt(nextIndex);if?(nextChar?!=?'{')?{throw?new?RuntimeException(String.format("這里應該是#{\nsql:%s\nindex:%d",?sqlBuilder.toString(),?nextIndex));}StringBuilder?argsStringBuilder?=?new?StringBuilder();i?=?parseSQLArg(argsStringBuilder,?sql,?nextIndex);String?argName?=?argsStringBuilder.toString();Object?argValue?=?argsMap.get(argName);if?(null?==?argValue)?{throw?new?RuntimeException(String.format("找不到參數值:%s",?argName));}sqlBuilder.append(argValue.toString());continue;}sqlBuilder.append(c);}return?sqlBuilder.toString();}private?static?int?parseSQLArg(StringBuilder?argsStringBuilder,?String?sql,?int?nextIndex)?{//?為啥 ?一次,因為現在nextIndex指向的是{,所以要 1找到{的下一位nextIndex? ?;for?(;?nextIndex?<?sql.length();?nextIndex? ?)?{char?c?=?sql.charAt(nextIndex);if?(c?!=?'}')?{argsStringBuilder.append(c);continue;}if?(c?==?'}')?{return?nextIndex;}}throw?new?RuntimeException(String.format("語法不對,缺少右括號('{')\nindex:%d",?nextIndex));}public?static?Map<String,?Object>?buildMethodArgsMap(Method?method,?Object[]?args)?{Map<String,?Object>?argsMap?=?new?HashMap<>();Parameter[]?parameters?=?method.getParameters();if?(parameters.length?!=?args.length)?{throw?new?RuntimeException("參數個數不一致呀,兄弟");}int[]?index?=?{0};Arrays.asList(parameters).forEach(parameter?->?{Param?paramAnno?=?parameter.getAnnotation(Param.class);String?name?=?paramAnno.value();System.out.println(name);argsMap.put(name,?args[index[0]]);index[0]? ?;});return?argsMap;} }2.3.7、測試
上面完整代碼的測試結果如下:
代理類生效了,方法名:listUser, 參數是:[1,?test] id name sql:?SELECT?*?FROM?user?WHERE?id?=?#{id}?AND?name?=?#{name} parseSQL:?SELECT?*?FROM?user?WHERE?id?=?1?AND?name?=?test很明顯發現我們完美的得到了想要的SQL,接下來jdbc,解析ResultSet就完事了。這里沒涉及。
我們故意寫錯SQL,去掉#后面的{,再看效果
修改UserMapper接口的listUser方法為如下
public?interface?UserMapper?{@Select("SELECT?*?FROM?user?WHERE?id?=?#id}?AND?name?=?#{name}")List<User>?listUser(@Param("id")?Integer?id,?@Param("name")?String?name); }輸出結果直接報錯了
Exception?in?thread?"main"?java.lang.RuntimeException:?這里應該是#{ sql:SELECT?*?FROM?user?WHERE?id?=? index:31at?com.chentongwei.mybatis.MybatisDemo.parseSQL(MybatisDemo.java:54)at?com.chentongwei.mybatis.MybatisDemo$1.invoke(MybatisDemo.java:34)at?com.sun.proxy.$Proxy0.listUser(Unknown?Source)at?com.chentongwei.mybatis.MybatisDemo.main(MybatisDemo.java:41)再次寫錯SQL,將@Param里的參數名和SQL的參數名寫的不一致,看效果:
public?interface?UserMapper?{@Select("SELECT?*?FROM?user?WHERE?id?=?#{id}?AND?name?=?#{name}")List<User>?listUser(@Param("id")?Integer?id,?@Param("name1")?String?name); } Exception?in?thread?"main"?java.lang.RuntimeException:?找不到參數值:nameat?com.chentongwei.mybatis.MybatisDemo.parseSQL(MybatisDemo.java:62)at?com.chentongwei.mybatis.MybatisDemo$1.invoke(MybatisDemo.java:34)at?com.sun.proxy.$Proxy0.listUser(Unknown?Source)at?com.chentongwei.mybatis.MybatisDemo.main(MybatisDemo.java:41)S3、總結
-
mybatis底層源碼肯定比這優化的很多,各種解析組件,不是for每個SQL的字符去拼接
-
實際mybatis底層有自己封裝好的異常,而不是直接RuntimeException
-
這里僅僅是為了演示原理,所以不涉及到JDBC執行、映射ResultSet到entity等
?三、幾張圖
實際mybatis源碼寫的很棒,各個組件封裝的很好,也很清晰,代友攔截器功能使之可插拔。
下面這個是比較詳細的mybatis核心組件圖
mybatis源碼包也見名知意
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的肝一波 ~ 手写一个简易版的Mybatis,带你深入领略它的魅力!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊大厂面试官必问的 MySQL 锁机制
- 下一篇: 万万没想到,一个可执行文件原来包含了这么