肝一波 ~ 手写一个简易版的Mybatis,带你深入领略它的魅力!
肝一波 ~ 手写一个简易版的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
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。
-
获取listUser方法的所有参数
-
获取每个参数的@Param注解的值,这个值就是map里的key
-
获取传进来的args[i]作为value
-
将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:%snindex:%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:%snindex:%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源码包也见名知意