当前位置: 首页 > news >正文

mybatis if test 用法_手写一个简易版的Mybatis,带你深入领略它的魅力

零、准备工作

              mysql      mysql-connector-java      8.0.20                  org.mybatis      mybatis      3.5.5                  org.projectlombok      lombok      1.18.12      provided    

一、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 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: * 

 * Project mybatis-source-study * * @author TongWei.Chen 2020-06-06 17:12:07 */@Data@NoArgsConstructor@AllArgsConstructorpublic 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: * 

 * 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 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 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 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 buildMethodArgsMap(Method method, Object[] args) {    // 最终参数-参数值都放到这里    Map 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 argsMap = new HashMap<>();    argsMap.put("id", 1);    argsMap.put("name", "test"); */public static String parseSQL(String sql, Map argsMap) {    StringBuilder sqlBuilder = new StringBuilder();    // 遍历sql的每一个字母,判断是不是#开头,是的话找到#{,然后请求parseSQLArg方法填充参数值(1,test)    for (int i = 0; i 

主要就干了下面这件事:

将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 

找到参数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: * 

 * 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 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 argsMap) {        StringBuilder sqlBuilder = new StringBuilder();        for (int i = 0; i  buildMethodArgsMap(Method method, Object[] args) {        Map 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]idnamesql: 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 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 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源码写的很棒,各个组件封装的很好,也很清晰,带有拦截器功能使之可插拔。

b51d1349e56c0edd350d52ce9977583e.png

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

f36d71d78185a48a5271305afe873b3b.png

mybatis源码包也见名知意

ea730b99340c16539860161de5869049.png

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

相关文章:

  • python写入excel数据时保存之前内容_如何使用python在保留原excel格式的前提下插入/修改数据...
  • asp.net web开发框架_ASP.NET Core Blazor未来的Web开发框架
  • 操作系统实验c语言页面置换算法(lru和lfu算法)_「任性」的C语言之父:因拒付论文装订费错失博士学位,论文52年后重见天日...
  • python生日快乐代码_【震惊小伙伴的单行代码—Python篇】的实践操作
  • c3p0连接池配置_数据库连接池amp;Spring JDBC(JdbcTemplate)
  • cdr自动排版插件_牛逼!网上卖328的CorelDraw插件免费送一键转曲批量导图文字识别...
  • python最接近某个值怎么表示_在python3中实现查找数组中最接近与某值的元素操作...
  • java连接rabbitmq_RabbitMQ指南之一:Hello World!
  • python if 单行_学python时,发现很多高手的代码只有一行或几行就达到了我多行代码一样的功能,应该追求这种简洁吗?...
  • python openpyxl读写xlsx_Python使用openpyxl读取、修改excel文件及绘chart图(支持xlsx)...
  • java array 元素的位置_JAVA《集合框架》
  • 植物图像识别python_python 实现图像识别
  • python开发博客系统_python 全栈开发,Day80(博客系统分析,博客主页展示)
  • python如何执行部分代码_Python 在局部变量域中执行代码
  • gradle jar 修改 output 路径_Gradle系列之Java Gradle插件
  • JS 中的深拷贝与浅拷贝
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • Android系统模拟器绘制实现概述
  • canvas绘制圆角头像
  • css属性的继承、初识值、计算值、当前值、应用值
  • HashMap剖析之内部结构
  • JavaScript学习总结——原型
  • Laravel 中的一个后期静态绑定
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 第十八天-企业应用架构模式-基本模式
  • 机器学习学习笔记一
  • 记一次和乔布斯合作最难忘的经历
  • 马上搞懂 GeoJSON
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 我的zsh配置, 2019最新方案
  • 一个完整Java Web项目背后的密码
  • 怎么将电脑中的声音录制成WAV格式
  • 自制字幕遮挡器
  • 2017年360最后一道编程题
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • (1)虚拟机的安装与使用,linux系统安装
  • (pojstep1.1.2)2654(直叙式模拟)
  • (每日持续更新)jdk api之StringBufferInputStream基础、应用、实战
  • (太强大了) - Linux 性能监控、测试、优化工具
  • ******之网络***——物理***
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .NET 的程序集加载上下文
  • .net反编译工具
  • .NET开源快速、强大、免费的电子表格组件
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .Net面试题4
  • .net与java建立WebService再互相调用
  • /etc/shadow字段详解
  • /usr/lib/mysql/plugin权限_给数据库增加密码策略遇到的权限问题
  • [ 第一章] JavaScript 简史
  • [20150321]索引空块的问题.txt
  • [Android]竖直滑动选择器WheelView的实现
  • [ASP.NET MVC]Ajax与CustomErrors的尴尬