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

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 肝一波 ~ 手写一个简易版的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.forNameDriverManager.getConnectionconnection.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。


  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源码写的很棒,各个组件封装的很好,也很清晰,代友拦截器功能使之可插拔。


image.png


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


image.png


mybatis源码包也见名知意


image.png


END

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
5月前
|
Java 数据库连接 程序员
从头到尾手把手教你搭建阅读Mybatis源码的环境(程序员必备技能)
从头到尾手把手教你搭建阅读Mybatis源码的环境(程序员必备技能)
98 0
|
5月前
|
SQL Java 数据库连接
解锁数据库操作新境界:轻松上手的MyBatis快速入门指南
解锁数据库操作新境界:轻松上手的MyBatis快速入门指南
19 0
|
6月前
|
SQL Java 数据库连接
[推荐] MyBatis框架初学笔记-为之后拾遗
[推荐] MyBatis框架初学笔记-为之后拾遗
40 0
|
10月前
|
SQL XML Java
硬核手写简易mybatis框架
硬核手写简易mybatis框架
|
12月前
|
Java 数据库连接 数据库
返璞归真,学了那么多技术栈,那些 [Mybatis-plus] 之 CRUD操作你还熟悉吗
返璞归真,学了那么多技术栈,那些 [Mybatis-plus] 之 CRUD操作你还熟悉吗
62 0
|
12月前
|
SQL 算法 Java
[兔子私房课] Mybatis开发详解与项目实战02
大家好,本章主要讲解mybatisPlus的日志配置和ID生成策略!
|
12月前
|
SQL 监控 druid
小白救星-SpringBoot最简教程06:web开发实战
先找到这个类:WebMvcAutoConfiguration,在这个jar包里面,还是自动配置
|
SQL XML 存储
假装是小白之重学MyBatis(一)
假装是小白之重学MyBatis(一)
假装是小白之重学MyBatis(一)
|
SQL 监控 Java
假装是小白之重学MyBatis(二)
假装是小白之重学MyBatis(二)
假装是小白之重学MyBatis(二)
|
SQL XML 存储
【SSM直击大厂】第十二章:MyBatis 快速入门
📋📋 精彩摘要:MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
91 0