天天看點

自己實作Mybatis底層機制

作者:黑芝麻湯圓他爹

主要实现:封装SqlSession到执行器+Mapper接口和Mapper.xml+MapperBean+动态代理Mapper的方法

1.Mybatis整体架构分析

自己實作Mybatis底層機制

对上图的解读:

1)mybatis 的核心配置文件

mybatis-config.xml:进行全局配置,全局只能有一个这样的配置文件

​ XxxMapper.xml 配置多个SQL,可以有多个 XxxMapper.xml 配置文件

2)通过 mybatis-config.xml 配置文件得到 SqlSessionFactory

3)通过 SqlSessionFactory 得到 SqlSession,用 SqlSession 就可以操作数据了

4)SqlSession 底层是 Executor(执行器),有两个重要的实现类

自己實作Mybatis底層機制

5)MappedStatement 是通过 XxxMapper.xml 来定义的,用来生成 statement 对象

6)参数输入执行并输出结果集,无需动手判断参数类型和参数下标位置,且自动将结果集映射为Java对象

2.搭建开发环境

(1)创建maven项目

自己實作Mybatis底層機制
自己實作Mybatis底層機制

(2)在pom.xml 中引入必要的依赖

<!--指定编译器/source/target的版本--><properties>    <project.build.sourdeEncoding>UTF-8</project.build.sourdeEncoding>    <maven.compiler.source>1.8</maven.compiler.source>    <maven.compiler.target>1.8</maven.compiler.target>    <java.version>1.8</java.version></properties> <!--引入必要的依赖--><dependencies>    <!--dom4j-->    <dependency>        <groupId>dom4j</groupId>        <artifactId>dom4j</artifactId>        <version>1.6.1</version>    </dependency>    <!--mysql-->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>5.1.49</version>    </dependency>    <!--lombok-简化entity/javabean/pojo 的开发-->    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>        <version>1.18.4</version>    </dependency>    <!--junit-->    <dependency>        <groupId>junit</groupId>        <artifactId>junit</artifactId>        <version>4.12</version>    </dependency></dependencies>           

(3)创建数据库和表

-- 创建数据库CREATE DATABASE `li_mybatis`;USE `li_mybatis`;-- 创建monster表CREATE TABLE `monster`(`id` INT NOT NULL AUTO_INCREMENT,`age` INT NOT NULL,`birthday` DATE DEFAULT NULL,`email` VARCHAR(255) NOT NULL,`gender` TINYINT NOT NULL,-- 1 male,0 female`name` VARCHAR(255) NOT NULL,`salary` DOUBLE NOT NULL,PRIMARY KEY(`id`))CHARSET=utf8-- insertINSERT INTO `monster` VALUES(NULL,200,'2000-11-11','[email protected]',1,'牛魔王',8888);           
自己實作Mybatis底層機制

3.设计思路

自己實作Mybatis底層機制

解读:

  1. 传统的方式操作数据库

    1)得到 MySession 对象

    2)调用 MyExecutor 的方法完成操作

    3)MyExecutor 的连接是从 MyConfiguration 获取

  2. Mybatis 操作数据库的方式

    1)得到 MySession 对象

    2)不直接调用 MyExecutor 的方法

    3)而是通过 MyMapperProxy 获取 Mapper 对象

    4)调用 Mapper 的方法,完成对数据库的操作

    5)Mapper 最终还是动态代理方式,使用 MyExecutor 的方法完成操作

    6)这里比较麻烦的就是 MyMapperProxy 的动态代理机制如何实现

4.任务阶段1

阶段1任务:通过配置文件,获取数据库连接

4.1分析

自己實作Mybatis底層機制

4.2代码实现

(1)在src 的 resources目录下创建 my-config.xml,模拟原生的 mybatis 配置文件

<?xml version="1.0" encoding="UTF-8" ?><database>    <!--配置连接数据库的信息-->    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>    <property name="url" value="jdbc:mysql://localhost:3306/li_mybatis?    useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>    <property name="username" value="root"/>    <property name="password" value="123456"/></database>           

(2)创建 MyConfiguration 类,用来读取xml文件,建立连接

因为这里重点是实现 Mybatis 的底层机制,为了简化操作,就不使用数据库连接池了,直接使用原生的connection 连接
package com.li.limybatis.sqlsession; import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader; import java.io.InputStream;import java.sql.Connection;import java.sql.DriverManager; /** * @author 李 * @version 1.0 * 用来读取xml文件,建立连接 */public class MyConfiguration {    //属性-类的加载器    private static ClassLoader loader = ClassLoader.getSystemClassLoader();     //读取xml文件并处理    public Connection build(String resource) {        Connection connection = null;        try {            //先加载配置文件 my-config.xml,获取对应的InputStream            InputStream stream = loader.getResourceAsStream(resource);            //解析 my-config.xml文件            SAXReader reader = new SAXReader();            Document document = reader.read(stream);            //获取 xml文件的根元素 <database>            Element root = document.getRootElement();            System.out.println("root=" + root);            //根据root解析,获取Connection            connection = evalDataSource(root);        } catch (Exception e) {            e.printStackTrace();        }        return connection;    }     //解析 my-config.xml 的信息,并返回 Connection    private Connection evalDataSource(Element node) {        if (!"database".equals(node.getName())) {            throw new RuntimeException("root节点应该是<database>");        }         //连接DB的必要参数        String driverClassName = null;        String url = null;        String username = null;        String password = null;         //遍历node下的子节点,获取其属性值        for (Object item : node.elements("property")) {            //i就是对应的 property节点            Element i = (Element) item;            //property节点的 name属性的值            String name = i.attributeValue("name");            //property节点的 value属性的值            String value = i.attributeValue("value");             //判断值是否为空            if (name == null || value == null) {                throw new RuntimeException("property节点没有设置name或value属性!");            }            switch (name) {                case "url":                    url = value;                    break;                case "username":                    username = value;                    break;                case "driverClassName":                    driverClassName = value;                    break;                case "password":                    password = value;                    break;                default:                    throw new RuntimeException("属性名没有匹配到..");            }        }        //获取连接        Connection connection = null;        try {            Class.forName(driverClassName);            connection = DriverManager.getConnection(url, username, password);        } catch (Exception e) {            e.printStackTrace();        }        return connection;    }}           

5.任务阶段2

阶段2任务:通过实现执行器机制,对数据表进行操作

5.1分析

我们把对数据库的操作封装到一套Executor机制中,程序具有更好的拓展性,结构更加清晰。这里我们先实现传统的方式连接数据库,即通过MyExecutor直接操作数据库。

自己實作Mybatis底層機制

5.2代码实现

(1)生成 entity 类 Monster.java

package com.li.entity; import lombok.*; import java.util.Date; /** * @author 李 * @version 1.0 * Monster类和 monster有映射关系 * * 注解说明: * @Getter 给所有属性生成 getter方法 * @Setter 给所有属性生成 setter方法 * @ToString 生成toString方法 * @NoArgsConstructor 生成一个无参构造器 * @AllArgsConstructor 生成一个全参构造器 * @Data 会生成上述除了无参/全参构造器的所有方法,此外还会生成equals,hashCode等方法 */@Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublic class Monster {        private Integer id;    private Integer age;    private String name;    private String email;    private Date birthday;    private double salary;    private Integer gender;}           

(2)Executor 接口

package com.li.limybatis.sqlsession; /** * @author 李 * @version 1.0 */public interface Executor {    //泛型方法    public <T> T query(String statement, Object parameter);}           

(3)执行器实现类 MyExecutor.java

package com.li.limybatis.sqlsession; import com.li.entity.Monster; import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException; /** * @author 李 * @version 1.0 */public class MyExecutor implements Executor {    private MyConfiguration myConfiguration = new MyConfiguration();     /**     * 根据sql,返回查询结果     *     * @param sql     * @param parameter     * @param <T>     * @return     */    @Override    public <T> T query(String sql, Object parameter) {        //获取连接对象        Connection connection = getConnection();        //查询返回的结果集        ResultSet set = null;        PreparedStatement pre = null;        try {            //构建PreparedStatement对象            pre = connection.prepareStatement(sql);            //设置参数,如果参数多,可以使用数组处理            pre.setString(1, parameter.toString());            //查询返回的结果集            set = pre.executeQuery();            //把结果集的数据封装到对象中-monster            //说明:这里做了简化处理,认为返回的结果就是一个monster记录,完善的写法应该使用反射机制            Monster monster = new Monster();            //遍历结果集,将数据封装到monster对象中            while (set.next()) {                monster.setId(set.getInt("id"));                monster.setName(set.getString("name"));                monster.setEmail(set.getString("email"));                monster.setAge(set.getInt("age"));                monster.setGender(set.getInt("gender"));                monster.setBirthday(set.getDate("birthday"));                monster.setSalary(set.getDouble("salary"));            }            return (T) monster;         } catch (Exception e) {            e.printStackTrace();        } finally {            try {                if (set != null) {                    set.close();                }                if (pre != null) {                    pre.close();                }                if (connection != null) {                    connection.close();                }            } catch (Exception e) {                e.printStackTrace();            }        }        return null;    }     //编写方法,通过myConfiguration对象返回连接    private Connection getConnection() {        Connection connection = myConfiguration.build("my-config.xml");        return connection;    }}           

(4)进行测试

@Testpublic void query() {    Executor executor = new MyExecutor();    Monster monster =            (Monster) executor.query("select * from monster where id = ?", 1);    System.out.println("monster--" + monster);}           

测试结果:

自己實作Mybatis底層機制

6.任务阶段3

阶段3任务:将执行器封装到SqlSession

6.1代码实现

(1)创建 MySqlSession 类,将执行器封装到SqlSession中。

package com.li.limybatis.sqlsession; /** * @author 李 * @version 1.0 * MySqlSession:搭建Configuration(连接)和Executor之间的桥梁 */public class MySqlSession {    //执行器    private Executor executor = new MyExecutor();    //配置    private MyConfiguration myConfiguration = new MyConfiguration();     //编写方法selectOne,返回一条记录    public <T> T selectOne(String statement,Object parameter){        return executor.query(statement, parameter);    }}           

(2)测试

@Testpublic void selectOne() {    MySqlSession mySqlSession = new MySqlSession();    Monster monster =            (Monster) mySqlSession.selectOne("select * from monster where id=?", 1);    System.out.println("monster=" + monster);}           

测试结果:

自己實作Mybatis底層機制

7.任务阶段4&5

阶段4任务:开发Mapper接口和Mapper.xml

阶段5任务:开发和Mapper接口相映射的MapperBean

自己實作Mybatis底層機制

(1)Mapper接口

package com.li.mapper; import com.li.entity.Monster; /** * @author 李 * @version 1.0 * MonsterMapper:声明对数据库的crud方法 */public interface MonsterMapper {    //查询方法    public Monster getMonsterById(Integer id); }           

(2)Mapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.li.mapper.MonsterMapper">    <!--实现配置接口方法getMonsterById-->    <select id="getMonsterById" resultType="com.li.entity.Monster">        select * from monster where id = ?    </select></mapper>           

(3)Function.java,用于记录Mapper.xml文件实现的方法信息

package com.li.limybatis.config; import lombok.Getter;import lombok.Setter; /** * @author 李 * @version 1.0 * Function:记录对应 Mapper.xml的方法信息 */@Getter@Setter@ToStringpublic class Function {    private String sqlType;//sql类型,如select,update,insert,delete    private String funcName;//方法名    private String sql;//执行的sql语句    private Object resultType;//返回类型    private String parameterType;//参数类型}           

(4)MapperBean.java,作用是读取Mapper接口对应的Mapper.xml,将该xml文件方法信息封装到MapperBean中。

package com.li.limybatis.config; import lombok.Getter;import lombok.Setter; import java.util.List; /** * @author 李 * @version 1.0 * MapperBean:将我们的Mapper信息,进行封装 */@Setter@Getter@ToStringpublic class MapperBean {    private String interfaceName;//接口名    //接口下的所有方法    public List<Function> functions;}           

8.任务阶段6

阶段6任务:在MyConfiguration中读取xxMapper.xml,能够创建MapperBean对象

(1)修改 MyConfiguration.java,添加 readMapper() 方法

/** * 读取xxMapper.xml,创建MapperBean对象 * @param path xml的路径+文件名,从类的加载路径开始计算,若xml文件放在resource目录下,直接传入文件名即可 * @return 返回MapperBean对象 */public MapperBean readMapper(String path) {    MapperBean mapperBean = new MapperBean();    try {        //获取到mapper.xml文件对应的InputStream        InputStream stream = loader.getResourceAsStream(path);        SAXReader reader = new SAXReader();        //获取到xml文件对应的document        Document document = reader.read(stream);        //得到xml的根节点        Element root = document.getRootElement();        //获取到 namespace        String namespace = root.attributeValue("namespace").trim();        //设置mapperBean的属性interfaceName        mapperBean.setInterfaceName(namespace);        //遍历获取root的子节点-生成 Function        Iterator rootIterator = root.elementIterator();        //保存接口下的所有方法信息        List<Function> list = new ArrayList<>();        while (rootIterator.hasNext()) {            //取出一个子元素            /**             * <select id="getMonsterById" resultType="com.li.entity.Monster">             *       select * from monster where id = ?             * </select>             */            Element e = (Element) rootIterator.next();            Function function = new Function();            String sqlType = e.getName().trim();            String funcName = e.attributeValue("id").trim();            //这里的resultType是返回类型的全路径-全类名            String resultType = e.attributeValue("resultType").trim();            String sql = e.getText().trim();            //将信息封装到 function对象中            function.setSql(sql);            function.setFuncName(funcName);            function.setSqlType(sqlType);            //这里的function.resultType应该为Object类型            //因此使用反射生成对象,再放入function中            Object instance = Class.forName(resultType).newInstance();            function.setResultType(instance);            //将封装好的function对象放到list中            list.add(function);        }         mapperBean.setFunctions(list);    } catch (Exception e) {        e.printStackTrace();    }    return mapperBean;}           

(2)测试

@Testpublic void readMapper() {    MyConfiguration myConfiguration = new MyConfiguration();    MapperBean mapperBean = myConfiguration.readMapper("MonsterMapper.xml");    System.out.println("mapperBean=" + mapperBean);}           

测试结果:

mapperBean=MapperBean(interfaceName=com.li.mapper.MonsterMapper, functions=[Function(sqlType=select, funcName=getMonsterById, sql=select * from monster where id = ?, resultType=Monster(id=null, age=null, name=null, email=null, birthday=null, salary=0.0, gender=null), parameterType=null)])           

9.任务阶段7

阶段7任务:实现动态代理Mapper的方法-动态代理生成Mapper对象,调用MyExecutor方法

自己實作Mybatis底層機制

(1)MyMapperProxy.java

package com.li.limybatis.sqlsession; import com.li.limybatis.config.Function;import com.li.limybatis.config.MapperBean; import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.List; /** * @author 李 * @version 1.0 * MyMapperProxy:动态代理生成 Mapper对象,调用 MyExecutor方法 */public class MyMapperProxy implements InvocationHandler {    private MySqlSession mySqlSession;    private String mapperFile;    private MyConfiguration myConfiguration;     //构造器    public MyMapperProxy(MySqlSession mySqlSession, MyConfiguration myConfiguration, Class clazz) {        this.mySqlSession = mySqlSession;        this.myConfiguration = myConfiguration;        this.mapperFile = clazz.getSimpleName() + ".xml";    }     //当执行Mapper接口的代理对象方法时,会执行到invoke方法    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        MapperBean mapperBean = myConfiguration.readMapper(this.mapperFile);        //判断是否是xml文件对应的接口        if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName()))         {            //通过method拿到执行的方法所在的接口的名称,与MapperBean存放的接口名比较            return null;        }        //取出MapperBean的functions        List<Function> functions = mapperBean.getFunctions();        //判断当前mapperBean解析对应的XML文件后,有方法        if (null != functions && 0 != functions.size()) {            for (Function function : functions) {                //如果当前要执行的方法和function.getFuncName()一样                //说明我们可以从当前遍历的function对象中,取出相应的信息sql,并执行方法                if (method.getName().equals(function.getFuncName())) {                    //如果当前function要执行的SqlType是select,就去执行selectOne                    /*                     * 说明:                     * 1.如果要执行的方法是select,就对应执行selectOne                     *   因为我们在MySqlSession只写了一个方法(selectOne)                     * 2.实际上原生的MySqlSession中应该有很多的方法,只是这里简化了,                     *    实际上应该根据不同的匹配情况调用不同的方法,并且还需要进行参数解析处理,                     *    还有比较复杂的字符串处理,拼接sql,处理返回类型等工作                     * 3.因为这里主要想实现mybatis生成mapper动态代理对象,调用方法的机制,所以简化                     */                    if ("select".equalsIgnoreCase(function.getSqlType())) {                        return mySqlSession                                .selectOne(function.getSql(), String.valueOf(args[0]));                    }                }            }        }        return null;    }}           

(2)修改MySqlSession.java,添加方法,返回动态代理对象

/** * 1.回 mapper的动态代理对象 * 2.这里的 clazz到时传入的类似 MonsterMapper.class * 3.返回的就是 MonsterMapper 接口的代理对象 * 4.当执行接口方法时(通过代理对象调用), *   根据动态代理机制会执行到MyMapperProxy的invoke()方法 * @param clazz * @param <T> * @return */public <T> T getMapper(Class<T> clazz) {    //返回动态代理对象    return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},            new MyMapperProxy(this, myConfiguration, clazz));}           

(3)创建 MySessionFactory.java

package com.li.limybatis.sqlsession; /** * @author 李 * @version 1.0 * MySessionFactory-会话工厂-返回会话SqlSession */public class MySessionFactory {    public static MySqlSession openSession() {        return new MySqlSession();    }}           

(4)测试

@Testpublic void openSession() {    MySqlSession mySqlSession = MySessionFactory.openSession();    MonsterMapper mapper = mySqlSession.getMapper(MonsterMapper.class);    System.out.println("mapper的运行类型=" + mapper.getClass());    Monster monster = mapper.getMonsterById(1);    System.out.println("monster--" + monster);}           
自己實作Mybatis底層機制