MyBatis底层机制示意图

mybatis-config.xml
- mybatis-config.xml 是MyBatis全局配置文件,在项目中只能有一份。
- 通过该配置文件可以得到SqlSessionFactory对象
SqlSessionFactory
- 通过SqlSessionFactory可以得到SqlSession,拿到SqlSession就可以操作数据库了。
SqlSession
SqlSession底层是Excutor执行器。
Excutor
- Excutor是接口,定义了很多方法。
- 有两个重要的实现类,基本执行器 BaseExcutor和缓存执行器 CacheExcutor。
  
Mapped Statement
- 对sql语句的参数及执行结果进行封装。
手写MyBatis框架
1. 思路分析

- 传统方式,通过Connection连接获取PrepareStatement对象,就可以执行sql了
- 使用MyBatis框架思想,则在执行sql之前,从mapper.xml中拿到sql和参数,结果类型。然后封装sql语句。执行sql之后,把得到结果封装到对应的类型之中。(动态代理实现)
2. 搭建Maven项目
配置pom.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.example</groupId>
    <artifactId>mybatis_edu</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--解析xml-->
        <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>
        <!--简化java bean开发-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
        </dependency>
    </dependencies>
</project>
阶段一 :读取配置文件,获取连接

(1)配置文件mybatis-config.xml简化版
<?xml version="1.0" encoding="UTF-8" ?>
<database>
 <!--配置数据库连接信息-->
 <property name="driver" value="com.mysql.jdbc.Driver"/>
 <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
 <property name="username" value="root"/>
 <property name="password" value="root1234"/>
</database>
(2)ConfigurationEdu,模拟Configuration
package com.mybatis.sqlsession;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
 * 读取xml,建立连接
 */
public class ConfigurationEdu {
    // 类加载器 ,作用:可以通过类加载器加载配置文件得到对应的流
    private ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    // 读取配置文件并处理
    public Connection build(String resource) {
        Connection connection = null;
        try {
            // 加载配置文件,获取对应inputStream流
            InputStream inputStream = classLoader.getResourceAsStream("mybatis-config.xml");
            // 解析xml dom4j
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(inputStream);
            // 获取配置文件根元素
            Element root = document.getRootElement();
            connection = evalElement(root);
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
    private Connection evalElement(Element node){
        String driver = "",url = "",username = "",password = "";
        Connection connection = null;
        if (!"database".equals(node.getName())){
            throw new RuntimeException("root 节点为 <database>");
        }
        // 遍历node节点下的字节点,获取属性值
        for (Object item : node.elements()){
            // e 就是 property
            Element e = (Element)item;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            // 判断是否得到 name 和 value
            if( null == name || null == value){
                throw new RuntimeException("未设置name和 value值");
            }
            switch (name){
                case "url" :
                    url = value;
                    break;
                case "username":
                    username = value;
                    break;
                case "password" :
                    password = value;
                    break;
                case "driver":
                    driver = value;
                    break;
                default:
                    throw new RuntimeException("未匹配到属性值。");
            }
        }
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}
(3)测试
测试一下,看看能不能获取到连接

阶段二:创建执行器Excutor
(1)通过Lombok创建Monster
package com.mybatis.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Monster {
    private Integer id;
    private Integer age;
    private String name;
    private String email;
    private Date birthday;
    private double salary;
}
(2)Executor接口
package com.mybatis.sqlsession;
/**
 * 执行器接口 定义操作数据库的通用方法
 */
public interface Executor {
    <T> T query(String sql, Object... params);
}
(3)BaseExecutor
package com.mybatis.sqlsession;
import com.mybatis.entity.Monster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
 * 编写执行器
 *      输入sql语句,完成数据库操作
 */
public class BaseExecutor implements Executor{
	
    private ConfigurationEdu configuration = new ConfigurationEdu();
    @Override
    public <T> T query(String sql, Object... params) {
        Connection connection = getConnection();
        PreparedStatement pre = null;
        ResultSet resultSet = null;
        Monster monster = null;
        try {
            pre = connection.prepareStatement(sql);
            if (params != null || params.length > 0){
                for (int i = 0; i < params.length; i++) {
                    pre.setObject(i+1,params[i]);
                }
            }
            resultSet = pre.executeQuery();
            // 封装结果集,底层用反射,在这里先简化封装
            monster = new Monster();
            while(resultSet.next()){
                monster.setId(resultSet.getInt("id"));
                monster.setAge(resultSet.getInt("age"));
                monster.setBirthday(resultSet.getDate("birthday"));
                monster.setName(resultSet.getString("name"));
                monster.setEmail(resultSet.getString("email"));
                monster.setSalary(resultSet.getDouble("salary"));
            }
            return (T) monster;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (resultSet != null){
                    resultSet.close();
                }
                if (pre != null){
                    pre.close();
                }
                if (connection != null){
                    connection.close();
                }
            }catch (Exception e){
                throw new RuntimeException("资源关闭失败。");
            }
        }
    }
    private Connection getConnection(){
        Connection connection = configuration.build("mybatis-config.xml");
        return connection;
    }
}
(4) Junit测试

阶段三:编写SqlSession

(1)编写SqlSession
package com.mybatis.sqlsession;
public class SqlSession {
    private Executor executor = new BaseExecutor();
    private ConfigurationEdu configuration = new ConfigurationEdu();
    public <T> T selectOne(String sql,Object... object){
        return executor.query(sql,object);
    }
}
(2) Junit测试

阶段四 : 编写MonsterMapper接口与MonsterMapper.xml
(1)MonsterMapper接口
package com.mybatis.mapper;
import com.mybatis.entity.Monster;
/**
 * 声明对monster表的CRUD
 */
public interface MonsterMapper {
    Monster getMonsterById(Integer id);
}
(2)MonsterMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.mybatis.mapper.MonsterMapper">
	<select id="getMonsterById" resultType="com.mybatis.entity.Monster">
        select * from `monster` where id = ?
	</select>
</mapper>
阶段五 : 开发Function和MapperBean

传统方式 编写完接口之后,会编写实现类,调用实现类完成功能
MyBatis是通过XML配置文件,XML配置文件不可能像Java代码一样运行,因此需要一个类,封装Mapper接口中的方法,并去XML中找到对应的SQL保存到该对象中。
package com.mybatis.config;
/**
 * 记录Mapper.xml中对应的方法
 */
public class Function {
    private String sqlType; // insert、delete、update、select
    private String funName; // 方法名
    private String sql; // 执行sql
    private Object resultType; // 结果类型
    private Object parameterType; // 参数类型
    public Function() {
    }
    public String getSqlType() {
        return sqlType;
    }
    public void setSqlType(String sqlType) {
        this.sqlType = sqlType;
    }
    public String getFunName() {
        return funName;
    }
    public void setFunName(String funName) {
        this.funName = funName;
    }
    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 Object getParameterType() {
        return parameterType;
    }
    public void setParameterType(Object parameterType) {
        this.parameterType = parameterType;
    }
}
package com.mybatis.config;
import java.util.List;
/**
 * 封装Mapper接口信息
 */
public class MapperBean {
    private String interfaceName; // 保存接口全路径名
    private List<Function> functions; // 接口中的所有方法
    public MapperBean() {
    }
    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;
    }
}
阶段六 : 在Configuration中加载mapper.xml,创建MapperBean对象
(1)Configuration
package com.mybatis.sqlsession;
import com.mybatis.config.Function;
import com.mybatis.config.MapperBean;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
 * 读取xml,建立连接
 */
public class Configuration {
    // 类加载器 ,作用:可以通过类加载器加载配置文件得到对应的流
    private ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    // 读取配置文件并处理
    public Connection build(String resource) {
        Connection connection = null;
        try {
            // 加载配置文件,获取对应inputStream流
            InputStream inputStream = classLoader.getResourceAsStream("mybatis-config.xml");
            // 解析xml dom4j
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(inputStream);
            // 获取配置文件根元素
            Element root = document.getRootElement();
            connection = evalElement(root);
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
    private Connection evalElement(Element node){
        String driver = "",url = "",username = "",password = "";
        Connection connection = null;
        if (!"database".equals(node.getName())){
            throw new RuntimeException("root 节点为 <database>");
        }
        // 遍历node节点下的字节点,获取属性值
        for (Object item : node.elements()){
            // e 就是 property
            Element e = (Element)item;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            // 判断是否得到 name 和 value
            if( null == name || null == value){
                throw new RuntimeException("未设置name和 value值");
            }
            switch (name){
                case "url" :
                    url = value;
                    break;
                case "username":
                    username = value;
                    break;
                case "password" :
                    password = value;
                    break;
                case "driver":
                    driver = value;
                    break;
                default:
                    throw new RuntimeException("未匹配到属性值。");
            }
        }
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
    public MapperBean readMapper(String resource){
        InputStream inputStream = classLoader.getResourceAsStream(resource);
        SAXReader saxReader = new SAXReader();
        MapperBean mapperBean = null;
        try {
            Document document = saxReader.read(inputStream);
            Element root = document.getRootElement();
            System.out.println("root = " + root.getName());
            mapperBean = evalMapperElement(root);
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        return mapperBean;
    }
    private MapperBean evalMapperElement(Element node){
        if (!"mapper".equals(node.getName())){
            throw new RuntimeException("mapper 应为根节点");
        }
        MapperBean mapperBean = new MapperBean();
        Function function = null;
        String interfaceName = node.attributeValue("namespace");
        mapperBean.setInterfaceName(interfaceName);
        Iterator iterator = node.elementIterator();
        List<Function> functions = new ArrayList<>();
        while (iterator.hasNext()){
            function = new Function();
            Object next = iterator.next();
            Element e = (Element)next;
            function.setSqlType(e.getName().trim());
            function.setFunName(e.attributeValue("id").trim());
            //function.setParameterType(e.attributeValue("parameterType").trim());
            function.setSql(e.getText().trim());
            //function.setResultType(e.attributeValue("resultType").trim());
            // 反射生成resultType
            Object o = null;
            try {
                o = Class.forName(e.attributeValue("resultType").trim()).newInstance();
                function.setResultType(o);
            } catch (InstantiationException ex) {
                throw new RuntimeException(ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(ex);
            }
            functions.add(function);
        }
        mapperBean.setFunctions(functions);
        return mapperBean;
    }
}
(2)Junit测试

阶段七 : 实现动态代理执行Executor方法
(1)创建代理类
package com.mybatis.sqlsession;
import com.mybatis.config.Function;
import com.mybatis.config.MapperBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
/**
 * MapperProxy: 动态代理生成Mapper对象,调用HspExecutor方法
 */
public class MapperProxy implements InvocationHandler {
    //属性
    private SqlSession sqlSession;
    private String mapperFile;
    private Configuration configuration;
    //构造器
    public MapperProxy(Configuration configuration,
                          SqlSession sqlSession,
                          Class clazz) {
        this.configuration = configuration;
        this.sqlSession = sqlSession;
        this.mapperFile = "com/mybatis/mapper/"+clazz.getSimpleName() + ".xml";
    }
    //当执行Mapper接口的代理对象方法时,会执行到invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MapperBean mapperBean =
                configuration.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.getFunName())) {
                    //如果我们当前的function 要执行的sqlType是select
                    //我们就去执行selectOne
                    /**
                     *
                     * 老师说明:
                     * 1. 如果要执行的方法是select , 就对应执行selectOne
                     * 2. 因为老韩在HspSqlSession就写了一个 selectOne
                     * 3. 实际上HspSqlSession 应该对应不同的方法(多个方法)
                     * , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
                     * 4. 因为老韩主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化
                     */
                    if("select".equalsIgnoreCase(function.getSqlType())) {
                        return sqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
                    }
                }
            }
        }
        return null;
    }
}
(2)SqlSession中添加getMapper方法
package com.mybatis.sqlsession;
import java.lang.reflect.Proxy;
public class SqlSession {
    private Executor executor = new BaseExecutor();
    private Configuration configuration = new Configuration();
    public <T> T selectOne(String sql,Object... object){
        return executor.query(sql,object);
    }
    /**
     * 1. 返回mapper的动态代理对象
     * 2. 这里clazz 到时传入的是 MonsterMapper.class
     * 3. 返回的就是MonsterMapper接口代理对象
     * 4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> clazz) {
        //返回动态代理对象
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
                new MapperProxy(configuration,this,clazz));
    }
}
(3)Junit测试



















