likes
comments
collection
share

自己实现 MyBatis 底层机制

作者站长头像
站长
· 阅读数 69

自己实现 MyBatis 底层机制【封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理 代理 Mapper 的方法】

MyBatis 整体架构分析

自己实现 MyBatis 底层机制

对上图的解读

1) mybatis 的核心配置文件mybatis-config.xml: 进行全局配置,全局只能有一个这样的配置文件XxxMapper.xml 配置多个 SQL,可以有多个 XxxMappe.xml 配置文件

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

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

4) SqlSession 底层是 Executor(执行器), 有 2 个重要的实现类, 有很多很多方法

自己实现 MyBatis 底层机制 自己实现 MyBatis 底层机制

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

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

搭建 MyBatis 底层机制开发环境

创建maven项目

流程就不多说了上一篇和手写Spring 和Spring MVC已经介绍过了

配置xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wyxdu</groupId>
    <artifactId>wyx-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--定义编译器 / source / target 版本即可-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <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>
</project>

创建数据库和表

CREATE DATABASE `wyx_mybatis`;
USE `wyx_mybatis`;

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, 
`name` VARCHAR(255) NOT NULL, 
`salary` DOUBLE NOT NULL,
PRIMARY KEY (`id`)
) CHARSET=utf8

INSERT INTO `monster` VALUES(NULL, 200, '2000-11-11', 'nmw@sohu.com', 1,'牛魔王', 8888.88)

设计思路 

自己实现 MyBatis 底层机制


 实现任务阶段 1- 完成读取配置文件,得到数据库的连接

分析示意图 

自己实现 MyBatis 底层机制

 创建wyx_config.xml

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

创建WyxConfiguration类

package com.wyxdu.wyxmybatis.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;

/**
 * 读取xml文件, 建立连接
 */
public class WyxConfiguration {

    //属性-类的加载器
    private static ClassLoader loader =
            ClassLoader.getSystemClassLoader();

    //读取xml文件信息,并处理
    public Connection build(String resource) {

        Connection connection = null;

        try {
            //加载配置wyx_mybatis.xml 获取到对应的InputStream
            InputStream stream =
                    loader.getResourceAsStream(resource);
            //解析wyx_mybatis.xml  => dom4j
            SAXReader reader = new SAXReader();
            Document document = reader.read(stream);
            //获取到wyx_mybatis.xml 的根元素 <database>
            Element root = document.getRootElement();
            System.out.println("root=" + root);
            //解析root元素,返回Connection => 单独的编写一个方法
            connection = evalDataSource(root);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }

    //方法会解析wyx_config.xml 信息,并返回Connection
    //eval: 评估/解析
    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")) {
            Element i = (Element) item;//i 就是 对应property节点
            String name = i.attributeValue("name");
            String value = i.attributeValue("value");

            //判断是否得到name 和 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; //返回Connection
    }

}

完成测试

创建 WyxMyBatisTest测试类

自己实现 MyBatis 底层机制


实现任务阶段 2- 编写执行器,输入 SQL 语句,完成操作

分析示意图

自己实现 MyBatis 底层机制

自己实现 MyBatis 底层机制

创建Monster类

解读

@Getter 就会给所有属性 生成对应的getter

@Setter 就会给所有属性 生成对应的setter

@ToString 生成 toString...

@NoArgsConstructor 生成无参构造器

@AllArgsConstructor 生成要给全参构造器

@Data 注解相当于

Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value注解

package com.wyxdu.entity;

import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Date;

/**
 * Monster 和 monster表有映射关系
 *
 * 解读
 * @Getter 就会给所有属性 生成对应的getter
 * @Setter 就会给所有属性 生成对应的setter
 * @ToString 生成 toString...
 * @NoArgsConstructor 生成无参构造器
 * @AllArgsConstructor 生成要给全参构造器
 * @Data 注解相当于Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value注解
 * 如何选择主要还是看自己需要
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Monster {

    private Integer id;
    private Integer age;
    private String name;
    private String email;
    private Date birthday;
    private double salary;
    private Integer gender;

}

创建Executor.java类

public interface Executor {
    //泛型方法
    public <T> T query(String statement, Object parameter);
}

创建WyxExecutor实现类

package com.wyxdu.wyxmybatis.sqlsession;

import com.wyxdu.entity.Monster;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class WyxExecutor implements Executor {

    //属性
    private WyxConfiguration wyxConfiguration =
            new WyxConfiguration();


    /**
     * 根据 sql 查找结果
     *
     * @param sql
     * @param parameter
     * @param <T>
     * @return
     */
    @Override
    public <T> T query(String sql, Object parameter) {
        //得到连接Connection
        Connection connection = getConnection();
        //查询返回的结果集
        ResultSet set = null;
        PreparedStatement pre = null;

        try {
            pre = connection.prepareStatement(sql);
            //设置参数, 如果参数多, 可以使用数组处理.
            pre.setString(1, parameter.toString());
            set = pre.executeQuery();
            //把set数据封装到对象-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 throwables) {
            throwables.printStackTrace();
        } finally {
            try {
                if (set != null) {
                    set.close();
                }
                if (pre != null) {
                    pre.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (Exception throwables) {
                throwables.printStackTrace();
            }
        }

        return null;
    }

    //编写方法,通过WyxConfiguration对象,返回连接
    private Connection getConnection() {
        Connection connection =
                wyxConfiguration.build("wyx_mybatis.xml");
        return connection;
    }
}

测试效果

修改 WyxMyBatisTest测试类 增加方法

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

自己实现 MyBatis 底层机制


实现任务阶段 3- 将 Sqlsession 封装到执行器

 分析示意图, 先观察原生 MyBatis 的 SqlSession 接口和默认接口和默认实现

自己实现 MyBatis 底层机制

 流程图

自己实现 MyBatis 底层机制

创建 WyxSqlSession类

/**
 * WyxSqlSession: 搭建Configuration(连接)和Executor之间桥梁
 * 这里有操作DB的方法-
 */
public class WyxSqlSession {
    //属性
    //执行器
    private Executor executor = new WyxExecutor();
    //配置
    private WyxConfiguration wyxConfiguration =
            new WyxConfiguration();

    //编写方法SelectOne 返回一条记录-对象 [做了简化]
    //说明: 在原生的Mybatis中 statement 不是sql ,而是要执行的接口方法
    //这里是做简化
    public <T> T selectOne(String statement, Object parameter) {
        return executor.query(statement, parameter);
    }
}

完成测试 

修改 MonsterMapperTest增加测试方法

    @Test
    public void selectOne() {
        WyxSqlSession wyxSqlSession = new WyxSqlSession();
        Monster monster =
                wyxSqlSession.selectOne("select * from monster where id=?", 1);
        System.out.println("monster--" + monster);
    }

自己实现 MyBatis 底层机制

 


 

实现任务阶段 4- 开发 Mapper  接口和 Mapper.xml 

分析【示意图】 

自己实现 MyBatis 底层机制

 创建Monster Mapper.java

/**
 * MonsterMapper: 声明对db的crud方法
 */
public interface MonsterMapper {

    //查询方法
    public Monster getMonsterById(Integer id);
}

创建wyx_mybits.xml 

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

 


 

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

 分析示意图

自己实现 MyBatis 底层机制

 创建Function.java , 对应 Mapper 的方法信息

package com.wyxedu.wyxmybatis.config;

/**
 * Function: 记录对应的Mapper的方法信息
 */
public class Function {
    //属性
    private String sqlType; //sql类型,比如select,insert,update, delete
    private String funcName; //方法名
    private String sql;//执行的sql语句
    private Object resultType;//返回类型
    private String parameterType;//参数类型

    public String getSqlType() {
        return sqlType;
    }

    public void setSqlType(String sqlType) {
        this.sqlType = sqlType;
    }

    public String getFuncName() {
        return funcName;
    }

    public void setFuncName(String funcName) {
        this.funcName = funcName;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public Object getResultType() {
        return resultType;
    }

    public void setResultType(Object resultType) {
        this.resultType = resultType;
    }

    public String getParameterType() {
        return parameterType;
    }

    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }

    @Override
    public String toString() {
        return "Function{" +
                "sqlType='" + sqlType + '\'' +
                ", funcName='" + funcName + '\'' +
                ", sql='" + sql + '\'' +
                ", resultType=" + resultType +
                ", parameterType='" + parameterType + '\'' +
                '}';
    }
}

创建 MapperBean将mapper的信息 进行封装

package com.wyxedu.wyxmybatis.config;

import java.util.List;

/**
 * MapperBean: 将Mapper信息,进行封装
 */
public class MapperBean {
    private String interfaceName;//接口名

    //接口下的所有方法-集合
    private List<Function> functions;

    public String getInterfaceName() {
        return interfaceName;
    }

    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }

    public List<Function> getFunctions() {
        return functions;
    }

    public void setFunctions(List<Function> functions) {
        this.functions = functions;
    }

    @Override
    public String toString() {
        return "MapperBean{" +
                "interfaceName='" + interfaceName + '\'' +
                ", functions=" + functions +
                '}';
    }
}

 


 

实现任务阶段 6- 在 WyxConfiguration, 读取 XxxMapper.xml,能够创建 MappperBean 对象

 修改WyxConfiguration

    public MapperBean readMapper(String path) {

        MapperBean mapperBean = new MapperBean();

        try {
            //获取到xml文件对应的InputStream
            InputStream stream = loader.getResourceAsStream(path);
            SAXReader reader = new SAXReader();
            //获取到xml文件对应的document-dom4j
            Document document = reader.read(stream);
            //得到xml文件的根元素/根节点
            /*
            <mapper namespace="com.wyxedu.mapper.MonsterMapper">
                <!--实现配置接口方法getMonsterById-->
                <select id="getMonsterById" resultType="com.wyxedu.entity.Monster">
                    select * from monster where id = ?
                </select>
            </mapper>
             */
            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<>();
            //遍历它的子节点/子元素-生成Function
            while (rootIterator.hasNext()) {
                //取出一个子元素- dom4j Element
                /**
                 * <select id="getMonsterById" resultType="com.wyxedu.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.setSql(sql);
                function.setFuncName(funcName);
                function.setSqlType(sqlType);
                //这里多说一句 function-private Object resultType; 是resultType实例
                //使用反射生成一个对象, setResultType

                Object newInstance = Class.forName(resultType).newInstance();
                function.setResultType(newInstance);

                //将封装好的function对象加入到 list
                list.add(function);

            }
            //while循环结束后, 将function的list设置
            mapperBean.setFunctions(list);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return mapperBean;
    }

完成测试 

修改 MonsterMapperTest增加测试方法

    @Test
    public void readMapper() {

        WyxConfiguration wyxConfiguration = new WyxConfiguration();
        MapperBean mapperBean =
                wyxConfiguration.readMapper("MonsterMapper.xml");
        System.out.println("mapperBean---" + mapperBean);
        System.out.println("ok~~");
    }

自己实现 MyBatis 底层机制

 


 

实现任务阶段 7- 实现动态代理 Mapper 的方

自己实现 MyBatis 底层机制 

 创建WyxMapperProxy

package com.wyxedu.wyxmybatis.sqlsession;

import com.wyxedu.wyxmybatis.config.Function;
import com.wyxedu.wyxmybatis.config.MapperBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

/**
 * WyxMapperProxy: 动态代理生成Mapper对象,调用WyxExecutor方法
 */
public class WyxMapperProxy implements InvocationHandler {
    //属性
    private WyxSqlSession wyxSqlSession;
    private String mapperFile;
    private WyxConfiguration wyxConfiguration;

    //构造器
    public WyxMapperProxy(WyxConfiguration wyxConfiguration,
                          WyxSqlSession wyxSqlSession,
                          Class clazz) {

        this.wyxConfiguration = wyxConfiguration;
        this.wyxSqlSession = wyxSqlSession;
        this.mapperFile = clazz.getSimpleName() + ".xml";
    }


    //前面讲解spring 时,讲过动态代理机制=>提示, 如果忘记了动态代理知识
    //去spring的博客笔记和代码回顾
    //提示:当执行Mapper接口的代理对象方法时,会执行到invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        MapperBean mapperBean =
                wyxConfiguration.readMapper(this.mapperFile);

        //判断是否是xml文件对应的接口
        if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
            return null;
        }

        //取出mapperBean的functions
        List<Function> functions = mapperBean.getFunctions();
        //判断当前mapperBean解析对应MappperXML后 , 有方法
        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
                     * 2. 因为就在WyxSqlSession就写了一个 selectOne
                     * 3. 实际上WyxSqlSession 应该对应不同的方法(多个方法)
                     * , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
                     * 4. 因为主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化
                     */
                    if("select".equalsIgnoreCase(function.getSqlType())) {
                        return wyxSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
                    }
                }
            }
        }

        return null;
    }


}

说明:

1. 如果要执行的方法是select , 就对应执行selectOne

2. 因为就在WyxSqlSession就写了一个 selectOne

3. 实际上WyxSqlSession

应该对应不同的方法(多个方法) 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理,还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作

4. 因为主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化

修改WyxSqlsession.java 

1. 返回mapper的动态代理对象

2. 这里clazz 到时传入的是 MonsterMapper.class

3. 返回的就是MonsterMapper接口代理对象

4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke 

 如果感觉高亮警告不舒服可以在方法上设置@SuppressWarnings("unchecked")

 如果需要整个类没有警告可以使用 @SuppressWarnings("all")

    /**
     * 1. 返回mapper的动态代理对象
     * 2. 这里clazz 到时传入的是 MonsterMapper.class
     * 3. 返回的就是MonsterMapper接口代理对象
     * 4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke
     */
    public <T> T getMapper(Class<T> clazz) {

        //返回动态代理对象
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
                new WyxMapperProxy(wyxConfiguration,this,clazz));
    }

 创建WyxSessionFactory类

 会话工厂-返回会话WyxSqlSession

package com.wyxedu.wyxmybatis.sqlsession;

/**
 * WyxSessionFactory 会话工厂-返回会话WyxSqlSession
 */
public class WyxSessionFactory {

    public static WyxSqlSession openSession() {
        return new WyxSqlSession();
    }
}

完成测试 

修改 MonsterMapperTest增加测试方法

    @Test
    public void openSession() {

        WyxSqlSession wyxSqlSession = WyxSessionFactory.openSession();
        MonsterMapper mapper = wyxSqlSession.getMapper(MonsterMapper.class);
        Monster monster = mapper.getMonsterById(1);
        System.out.println("monster===" + monster);
    }

自己实现 MyBatis 底层机制

 

转载自:https://juejin.cn/post/7241148626814550074
评论
请登录