手写一个简易版的Mybatis,带你深入领略它的魅力

零、准备工作

 
<dependencies>     <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->     <dependency>       <groupId>mysql</groupId>       <artifactId>mysql-connector-java</artifactId>       <version>8.0.20</version>     </dependency>     <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->     <dependency>       <groupId>org.mybatis</groupId>       <artifactId>mybatis</artifactId>       <version>3.5.5</version>     </dependency>     <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->     <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() {         @Override         public 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

扫描二维码关注公众号,回复: 11645029 查看本文章

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注解,里面的值就是参数key         Param paramAnno = parameter.getAnnotation(Param.class);         // 获取参数值:id和name         String name = paramAnno.value();         System.out.println(name);         // 将参数值放到最终的map里。id:1、name:test         argsMap.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() {             @Override             public 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:31     at 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: 找不到参数值:name     at 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)S

3、总结

  • mybatis底层源码肯定比这优化的很多,各种解析组件,不是for每个SQL的字符去拼接
  • 实际mybatis底层有自己封装好的异常,而不是直接RuntimeException
  • 这里仅仅是为了演示原理,所以不涉及到JDBC执行、映射ResultSet到entity等

三、几张图

实际mybatis源码写的很棒,各个组件封装的很好,也很清晰,带有拦截器功能使之可插拔。

下面这个是比较详细的mybatis核心组件图

mybatis源码包也见名知意

资料在精不在多,收藏夹吃灰的已经够多了。重要的是认真去看,这些就够了。想要一起学习进步的,或者想要进学习群的都可以关注我私信我就拉你进群

猜你喜欢

转载自blog.csdn.net/weixin_49016003/article/details/107183795