MyBatis基本使用及原理剖析

MyBatis 是个持久层框架,以我曾经的眼光那些框架就是个辅助开发的工具,今年出个A,明年又出来个B,只要弄清楚原理来的再多也不过是老酒装新坛,换汤不换药的东西,但是遇到面试官刁钻的面试题就分分钟教你做人了!(我是谁?我在哪?555~) 所以本着做学问的态度,我觉得有必要剖析一下分享出来~

分享内容大致分为:MyBatis 基本使用,MyBatis 原理剖析,实现自己的 XXBatis

一、MyBatis基本使用

MyBatis采用ORM思想将实体类与数据库表进行映射,完成了数据库的持久化操作,其内部对JDBC进行了封装,使我们不再重复编写麻烦的数据库连接等相关代码,只关注SQL本身就好了~

1. 准备测试数据

首先创建数据库,生成数据库结构,插入测试数据。这里我使用的MySql数据库,测试库为test,表为test1

-- ----------------------------
-- Table structure for test1
-- ----------------------------
DROP TABLE IF EXISTS `test1`;
CREATE TABLE `test1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `content` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of test1
-- ----------------------------
INSERT INTO `test1` VALUES ('1', 'aaa', 'bbb');

2. 创建工程并导入jar包

为了方便管理jar包,这里使用Maven进行配置,直接pom.xml文件添加坐标即可

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.4.5</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.6</version>
  <scope>runtime</scope>
</dependency>

3. 编写实体类、接口、映射文件

实体类文件,需根据数据库表结构进行编写,总体比较简单

/**
 * Test1实体类
 *
 * @author Wenx
 * @date 2021/3/16
 */
public class Test1 implements Serializable {
    private Integer id;
    private String name;
    private String content;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "Test1{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

持久层接口,则根据实际业务需要进行编写,大致的增删改查是少不了的,这里为了演示只写了个查询方法

/**
 * Test1持久层接口
 *
 * @author Wenx
 * @date 2021/3/16
 */
public interface ITest1 {

    /**
     * 查询所有数据
     *
     * @return
     */
    List<Test1> findAll();

}

xml映射文件对应持久层接口进行编写即可。需注意该文件一般放在resources目录下,具有与持久层接口相同包名,该资源包目录与代码目录不同,需要一层层建立,否则不会建立com.xxxx.xxxx多级结构,而是名为com.xxxx.xxxx的一级目录

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wenx.demo.basis.mybatis.dao.ITest1">
    <!-- 配置查询所有操作 -->
    <select id="findAll" resultType="com.wenx.demo.basis.mybatis.entity.Test1">
        select * from test1
    </select>
</mapper>

4. 编写相关配置文件

创建SqlMapConfig.xml文件,配置下数据库连接及xml映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置 mybatis 的环境 -->
    <environments default="mysql">
        <!-- 配置 mysql 的环境 -->
        <environment id="mysql">
            <!-- 配置事务的类型 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置连接数据库的信息:用的是数据源(连接池) -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.126.129:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="1234@abcd"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 告知 mybatis 映射配置的位置 -->
    <mappers>
        <mapper resource="com/wenx/demo/basis/mybatis/dao/ITest1.xml"/>
        <!--<mapper class="com.wenx.demo.basis.mybatis.dao.ITest1"/>-->
    </mappers>
</configuration>

5. 编写Mybatis测试代码

Mybatis还有很多如注解、spring整合等使用方式,将会使开发更加简单,但这里为介绍原理仅演示Mybatis最常规的用法

/**
 * 原生Mybatis演示
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class MybatisDemo {
    public static void main(String[] args) throws Exception {
        // 1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2.创建 SqlSessionFactory 的构建者对象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        // 3.使用构建者创建工厂对象 SqlSessionFactory
        SqlSessionFactory factory = builder.build(in);
        // 4.使用 SqlSessionFactory 生产 SqlSession 对象
        SqlSession session = factory.openSession();
        // 5.使用 SqlSession 创建 dao 接口的代理对象
        ITest1 test1Dao = session.getMapper(ITest1.class);
        // 6.使用代理对象执行查询所有方法
        List<Test1> test1s = test1Dao.findAll();
        for (Test1 test1 : test1s) {
            System.out.println(test1);
        }
        // 7.释放资源
        session.close();
        in.close();
    }
}

二、MyBatis原理剖析

针对MyBatis测试代码我们一步步进行原理剖析,对其过程中运用到的编程思想和设计模式进行简明扼要的表述

1. 通过类加载器读取配置文件

读取配置文件是我们常用的功能,每个项目基本都会有很多配置,写在多个代码中改起来太麻烦,要是忘记写哪个文件里找起来更会让你痛不欲生,而将配置参数统一写到配置文件是必然的趋势,下面我们举例几种常见加载配置文件的方式

// MyBatis测试代码使用的读取配置文件方式,内部使用ClassLoder类加载器
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");


// 1.基于ClassLoder读取配置文件,该方式只能加载类路径下的配置文件
InputStream is1 = MybatisTest.class.getClassLoader().getResourceAsStream("SqlMapConfig.xml");

// 2.通过ServletContext的getRealPath方法获取绝对路径,该方式只能在servlet内使用
String realPath = ServletContext.getRealPath("SqlMapConfig.xml");
InputStream is2 = new FileInputStream(new File(realPath)); 

2. 创建构造者对象 SqlSessionFactoryBuilder

看到Builder后缀我们能想到构造者模式,对象的创建可能需要较多的配置参数,而对象构造过程相对复杂,构造者模式能够隐藏构造细节让使用者直接获取到对象。MyBatis构造者对象实际就是解析了SqlMapConfig.xmlITest1.xml文件,通过读取配置文件流将配置信息存到Configuration中,来构造出工厂对象SqlSessionFactory

// 创建 SqlSessionFactory 的构造者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

// 通过 build 构造出工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);

3. 创建工厂对象 SqlSessionFactory

看到Factory后缀我们能想到工厂模式,这个设计模式避免我们在代码中使用new来创建对象,防止由初始化对象方式改变而通篇去修改new代码,降低类之间的依赖关系达到解耦目的。另外还应用了单例模式,一旦被创建在应用的运行期间将一直存在并保持全局唯一,下面我们来看下其内部的SqlSessionFactory是如何创建的

// 内部通过 XMLConfigBuilder 类来解析xml配置文件生成 Configuration 对象,来构造出工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);

SqlSessionFactoryBuilderbuild方法

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException var13) {
        }
    }
    return var5;
}

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

4. 创建会话对象 SqlSession

SqlSessionFactory内部,加载配置文件中的配置信息来生产SqlSession对象

// 使用 SqlSessionFactory 生产 SqlSession 对象,而不用关心 SqlSession 对象是怎么 new 来的
// 我们 ctrl 键一路点下去,发现 DefaultSqlSessionFactory 类的 openSessionFromDataSource 方法
// 其中 Environment environment = this.configuration.getEnvironment(); 这句正是加载配置文件中的配置信息
// 而 Executor executor = this.configuration.newExecutor(tx, execType); 来创建 Executor 实例用于执行 SQL
SqlSession session = factory.openSession();

DefaultSqlSessionFactoryopenSessionFromDataSource方法

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }
    return var8;
}

ConfigurationnewExecutor方法,这里CachingExecutor用了装饰器模式,是否开启二级缓存,相当于不改变原有executor的情况下包装了层壳,来提供额外的功能

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? this.defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Object executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (this.cacheEnabled) {
        executor = new CachingExecutor((Executor)executor);
    }
    Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
    return executor;
}

5. 创建持久层接口的代理对象

使用SqlSession创建dao接口的代理对象,内部运用代理模式,在不改变源码前提下对已有方法进行增强。其实把面向对象的三种特性封装、继承、多态整明白了,设计模式就那么回事了~

// 使用 SqlSession 创建 dao 接口的代理对象,ctrl 键一路点下去我们可以发现其内部使用了动态代理,也可以理解为将持久层接口与mapper映射文件做了关联,调用接口方法实际获取了mapper映射文件的SQL来执行
ITest1 test1Dao = session.getMapper(ITest1.class);

MapperRegistrygetMapper方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}

MapperProxyFactorynewInstance方法,内部用了动态代理

protected T newInstance(MapperProxy<T> mapperProxy) {
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

6. 执行查询方法

使用代理对象执行查询方法

// 使用代理对象执行查询所有方法,实际获取了mapper映射文件的SQL来执行
List<Test1> test1s = test1Dao.findAll();
for (Test1 test1 : test1s) {
    System.out.println(test1);
}

7. 释放资源

执行完成后,可释放sql会话,释放配置文件流

// 释放资源
session.close();
in.close();

三、实现自己的XXBatis

上面一节我们对MyBatis原理进行了剖析,下面我们删除MyBatis包,创建与MyBatis保持相同的结构和类名来实现自己的XXBatis,看看是否可以达到MyBatis的效果

1. 编写XXBatis测试代码

XXBatis测试代码与原始原生Mybatis代码完全相同,但由于删除MyBatis包导致代码变红,很多类找不到,我们接下来逐步补全代码

package com.wenx.demo.basis.mybatis;

import com.wenx.demo.basis.mybatis.dao.ITest1;
import com.wenx.demo.basis.mybatis.entity.Test1;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactory;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactoryBuilder;
import com.wenx.demo.basis.mybatis.utils.Resources;

import java.io.InputStream;
import java.util.List;

/**
 * 自制XXbatis演示
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class XXbatisDemo {
    public static void main(String[] args) throws Exception {
        // 1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2.创建 SqlSessionFactory 的构建者对象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        // 3.使用构建者创建工厂对象 SqlSessionFactory
        SqlSessionFactory factory = builder.build(in);
        // 4.使用 SqlSessionFactory 生产 SqlSession 对象
        SqlSession session = factory.openSession();
        // 5.使用 SqlSession 创建 dao 接口的代理对象
        ITest1 test1Dao = session.getMapper(ITest1.class);
        // 6.使用代理对象执行查询所有方法
        List<Test1> test1s = test1Dao.findAll();
        for (Test1 test1 : test1s) {
            System.out.println(test1);
        }
        // 7.释放资源
        session.close();
        in.close();
    }
}

2. Resources加载配置文件

首先是Resources类,我们可以直接使用ClassLoader加载配置文件

package com.wenx.demo.basis.mybatis.utils;

import java.io.InputStream;

/**
 * 使用ClassLoader加载配置文件
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class Resources {

    /**
     * 获取文件字节输入流
     *
     * @param filePath 配置文件地址
     * @return
     */
    public static InputStream getResourceAsStream(String filePath) {
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }
}

3. SqlSessionFactoryBuilder构造者

接下来是SqlSessionFactoryBuilder,我们模仿mybatis结构,简化下代码保留核心类XMLConfigBuilderDefaultSqlSessionFactory

package com.wenx.demo.basis.mybatis.session;

import com.wenx.demo.basis.mybatis.session.defaults.DefaultSqlSessionFactory;
import com.wenx.demo.basis.mybatis.utils.XMLConfigBuilder;

import java.io.InputStream;

/**
 * SqlSessionFactory构造者
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class SqlSessionFactoryBuilder {

    /**
     * 构建SqlSessionFactory工厂对象
     *
     * @param inputStream 配置文件字节输入流
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream) {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream);
        return new DefaultSqlSessionFactory(parser.parse());
    }
}

4. XMLConfigBuilder配置解析

配置解析方式有2种,一种是XML方式,另一种是注解方式。XML我们使用dom4j+xpath来解析配置文件,注解我们用反射来弄,最后将配置信息填充到ConfigurationMappedStatement中去

package com.wenx.demo.basis.mybatis.utils;

import com.wenx.demo.basis.mybatis.annotations.Select;
import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.MappedStatement;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 解析XML配置文件
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class XMLConfigBuilder {
    private final InputStream inputStream;

    public XMLConfigBuilder(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    /**
     * 获得Configuration对象
     *
     * @return
     */
    public Configuration parse() {
        return parsingConfiguration(inputStream);
    }

    /**
     * 解析XML配置文件,将配置信息填充到Configuration中
     * 使用的技术:dom4j+xpath
     *
     * @param inputStream 配置文件字节输入流
     * @return 封装数据库连接信息和映射文件信息的Configuration对象
     */
    public static Configuration parsingConfiguration(InputStream inputStream) {
        try {
            // 定义封装数据库连接信息和映射文件信息的配置对象
            Configuration config = new Configuration();

            // 开始解析XML
            // 1.获取SAXReader对象
            SAXReader reader = new SAXReader();
            // 2.根据字节输入流获取Document对象
            Document document = reader.read(inputStream);
            // 3.获取根节点
            Element root = document.getRootElement();
            // 4.获取数据库配置节点,使用xpath中选择指定节点的方式,获取所有property节点
            List<Element> propertyElements = root.selectNodes("//property");
            // 5.遍历节点
            for (Element propertyElement : propertyElements) {
                // 取出节点的name属性的值,判断是否为数据库连接信息
                String name = propertyElement.attributeValue("name");
                if ("driver".equals(name)) {
                    // 表示驱动,获取property标签value属性的值
                    String driver = propertyElement.attributeValue("value");
                    config.setDriver(driver);
                }
                if ("url".equals(name)) {
                    // 表示连接字符串,获取property标签value属性的值
                    String url = propertyElement.attributeValue("value");
                    config.setUrl(url);
                }
                if ("username".equals(name)) {
                    // 表示用户名,获取property标签value属性的值
                    String username = propertyElement.attributeValue("value");
                    config.setUsername(username);
                }
                if ("password".equals(name)) {
                    // 表示密码,获取property标签value属性的值
                    String password = propertyElement.attributeValue("value");
                    config.setPassword(password);
                }
            }
            // 6.获取映射文件信息,取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            // 7.遍历集合
            for (Element mapperElement : mapperElements) {
                // 判断mapperElement使用的是哪个属性
                Attribute attribute = mapperElement.attribute("resource");
                if (attribute != null) {
                    // 表示有resource属性,使用的是XML,取出属性的值
                    String mapperPath = attribute.getValue();
                    // 把映射配置文件的内容获取出来,封装成一个map
                    Map<String, MappedStatement> mappers = parsingXML(mapperPath);
                    // 给configuration中的mappers赋值
                    config.setMappers(mappers);
                } else {
                    // 表示没有resource属性,使用的是注解,获取class属性的值
                    String daoClassPath = mapperElement.attributeValue("class");
                    // 根据daoClassPath获取封装的必要信息
                    Map<String, MappedStatement> mappers = parsingAnnotation(daoClassPath);
                    // 给configuration中的mappers赋值
                    config.setMappers(mappers);
                }
            }

            // 返回Configuration
            return config;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 解析XML,并且封装到Map中
     *
     * @param mapperPath 映射配置文件的位置
     * @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
     * 以及执行所需的必要信息(value是一个MappedStatement对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     * @throws IOException
     */
    private static Map<String, MappedStatement> parsingXML(String mapperPath) throws IOException {
        InputStream in = null;
        try {
            // 定义返回值对象
            Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();

            // 开始解析XML
            // 1.根据路径获取字节输入流
            in = Resources.getResourceAsStream(mapperPath);
            // 2.根据字节输入流获取Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            // 3.获取根节点
            Element root = document.getRootElement();
            // 4.获取根节点的namespace属性的值,作为map中key的前半部分
            String namespace = root.attributeValue("namespace");
            // 5.获取所有的select节点
            List<Element> selectElements = root.selectNodes("//select");
            // 6.遍历select节点集合
            for (Element selectElement : selectElements) {
                // 取出id属性的值,作为map中key的后半部分
                String id = selectElement.attributeValue("id");
                // 取出resultType属性的值,作为map中value的一部分
                String resultType = selectElement.attributeValue("resultType");
                // 取出文本内容,作为map中value的一部分
                String sqlStatement = selectElement.getText();
                // 创建Key
                String key = namespace + "." + id;
                // 创建Value
                MappedStatement mapper = new MappedStatement();
                mapper.setSqlStatement(sqlStatement);
                mapper.setResultType(resultType);
                // 把key和value存入mappers中
                mappers.put(key, mapper);
            }

            return mappers;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            in.close();
        }
    }

    /**
     * 解析dao中所有被select注解标注的方法,并且封装到Map中
     * 根据方法名称和类名,以及方法上注解value属性的值,作为MappedStatement的必要信息
     *
     * @param daoClassPath dao类文件的位置
     * @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
     * 以及执行所需的必要信息(value是一个MappedStatement对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     * @throws Exception
     */
    private static Map<String, MappedStatement> parsingAnnotation(String daoClassPath) throws Exception {
        // 定义返回值对象
        Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();

        // 1.得到dao接口的字节码对象
        Class daoClass = Class.forName(daoClassPath);
        // 2.得到dao接口中的方法数组
        Method[] methods = daoClass.getMethods();
        // 3.遍历Method数组
        for (Method method : methods) {
            // 取出每一个方法,判断是否有select注解
            boolean isAnnotated = method.isAnnotationPresent(Select.class);
            if (isAnnotated) {
                // 创建MappedStatement对象
                MappedStatement mapper = new MappedStatement();
                // 取出注解的value属性值
                Select selectAnnotation = method.getAnnotation(Select.class);
                String sqlStatement = selectAnnotation.value();
                mapper.setSqlStatement(sqlStatement);
                // 获取当前方法的返回值,还要求必须带有泛型信息 List<T>
                Type type = method.getGenericReturnType();
                // 判断type是不是参数化的类型
                if (type instanceof ParameterizedType) {
                    // 强转
                    ParameterizedType ptype = (ParameterizedType) type;
                    // 得到参数化类型中的实际类型参数
                    Type[] types = ptype.getActualTypeArguments();
                    // 取出第一个
                    Class domainClass = (Class) types[0];
                    // 获取domainClass的类名
                    String resultType = domainClass.getName();
                    // 给MappedStatement赋值
                    mapper.setResultType(resultType);
                }
                // 组装key的信息
                // 获取方法的名称
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String key = className + "." + methodName;
                // 给map赋值
                mappers.put(key, mapper);
            }
        }
        return mappers;
    }
}

5. Configuration配置类

我们从配置文件解析出来的配置信息,数据库连接信息与映射文件信息都存到配置类中,其中MappedStatement是映射文件配置信息

package com.wenx.demo.basis.mybatis.session;

import java.util.HashMap;
import java.util.Map;

/**
 * 自制xxbatis配置类
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class Configuration {
    /**
     * mysql数据库驱动
     */
    private String driver;
    /**
     * mysql数据库连接字符串
     */
    private String url;
    /**
     * mysql数据库用户名
     */
    private String username;
    /**
     * mysql数据库密码
     */
    private String password;
    /**
     * 映射文件配置信息
     */
    private Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();


    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Map<String, MappedStatement> getMappers() {
        return mappers;
    }

    public void setMappers(Map<String, MappedStatement> mappers) {
        // 使用追加的方式
        this.mappers.putAll(mappers);
    }
}

6. MappedStatement映射配置类

映射配置类我们主要用于存储映射XML文件中的SQL语句和返回结果实体类的全限定类名

package com.wenx.demo.basis.mybatis.session;

/**
 * 封装执行的SQL语句和结果类型的全限定类名
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class MappedStatement {
    /**
     * 执行的SQL语句
     */
    private String sqlStatement;
    /**
     * 结果类型实体类的全限定类名
     */
    private String resultType;


    public String getSqlStatement() {
        return sqlStatement;
    }

    public void setSqlStatement(String sqlStatement) {
        this.sqlStatement = sqlStatement;
    }

    public String getResultType() {
        return resultType;
    }

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

7. 注解方式的Select注解

使用注解方式可以省略映射XML文件,直接在dao接口类方法上使用注解填写SQL语句即可使用,但修改SQL语句需要重新编译,可谓是各有利弊需要看实际情况使用,这里编写个Select注解为测试使用

package com.wenx.demo.basis.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 查询语句的注解
 *
 * @author Wenx
 * @date 2021/3/17
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    /**
     * SQL语句
     *
     * @return
     */
    String value();
}

8. SqlSessionFactory接口

接下来的就是SqlSessionFactory接口,其中主要方法为openSession()方法

package com.wenx.demo.basis.mybatis.session;

/**
 * SqlSession工厂
 *
 * @author Wenx
 * @date 2021/3/17
 */
public interface SqlSessionFactory {

    /**
     * 打开一个会话,获得SqlSession对象
     *
     * @return
     */
    SqlSession openSession();
}

9. DefaultSqlSessionFactory实现类

DefaultSqlSessionFactory类为SqlSessionFactory接口的实现。JdbcTransaction类使用Jdbc建立数据库连接并操作事务,Executor类主要用来执行具体SQL语句,DefaultSqlSession类为SqlSession接口实现,用来创建dao接口的代理对象

package com.wenx.demo.basis.mybatis.session.defaults;

import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactory;
import com.wenx.demo.basis.mybatis.utils.Executor;
import com.wenx.demo.basis.mybatis.utils.JdbcTransaction;

/**
 * SqlSessionFactory接口实现
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 获取一个数据库连接会话
     *
     * @return
     */
    @Override
    public SqlSession openSession() {
        JdbcTransaction tx = new JdbcTransaction(configuration, false);
        Executor executor = new Executor(tx);
        return new DefaultSqlSession(configuration, executor);
    }
}

10. JdbcTransaction实现类

JdbcTransaction类为Transaction接口的实现,这里为减少复杂程度Transaction接口就没有创建,其内部是对Jdbc连接数据库操作的封装

package com.wenx.demo.basis.mybatis.utils;

import com.wenx.demo.basis.mybatis.session.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * Jdbc事务相关
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class JdbcTransaction {
    private final Configuration configuration;
    protected Connection connection;
    protected boolean autoCommmit;

    public JdbcTransaction(Configuration configuration, boolean desiredAutoCommit) {
        this.configuration = configuration;
        this.autoCommmit = desiredAutoCommit;
    }

    /**
     * 获取一个数据库连接
     *
     * @return
     * @throws SQLException
     */
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            this.openConnection();
        }

        return this.connection;
    }

    /**
     * 创建一个数据库连接
     *
     * @throws SQLException
     */
    protected void openConnection() throws SQLException {
        try {
            Class.forName(this.configuration.getDriver());
            this.connection = DriverManager.getConnection(
                    this.configuration.getUrl(),
                    this.configuration.getUsername(),
                    this.configuration.getPassword());
            this.connection.setAutoCommit(this.autoCommmit);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 事务提交
     *
     * @throws SQLException
     */
    public void commit() throws SQLException {
        if (this.connection != null && !this.connection.getAutoCommit()) {
            this.connection.commit();
        }

    }

    /**
     * 事务回滚
     *
     * @throws SQLException
     */
    public void rollback() throws SQLException {
        if (this.connection != null && !this.connection.getAutoCommit()) {
            this.connection.rollback();
        }

    }

    /**
     * 关闭数据库连接
     *
     * @throws SQLException
     */
    public void close() throws SQLException {
        if (this.connection != null) {
            this.resetAutoCommit();

            this.connection.close();
        }

    }

    /**
     * 重置事务自动提交
     */
    protected void resetAutoCommit() {
        try {
            if (!this.connection.getAutoCommit()) {
                this.connection.setAutoCommit(true);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

11. Executor类

Executor类主要用来执行具体SQL语句,实际就是Jdbc中操作PreparedStatement,执行executeQuery方法来运行SQL语句

package com.wenx.demo.basis.mybatis.utils;

import com.wenx.demo.basis.mybatis.session.MappedStatement;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 用于执行SQL语句
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class Executor {
    private final JdbcTransaction transaction;

    public Executor(JdbcTransaction transaction) {
        this.transaction = transaction;
    }

    public void close() {
        try {
            if (this.transaction != null) {
                this.transaction.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public <E> List<E> query(MappedStatement mapper) {
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            // 1.取出MappedStatement中的数据
            String sqlStatement = mapper.getSqlStatement();
            String resultType = mapper.getResultType();
            Class entityClass = Class.forName(resultType);
            // 2.获取PreparedStatement对象
            Connection conn = this.transaction.getConnection();
            pstmt = conn.prepareStatement(sqlStatement);
            // 3.执行SQL语句,获取结果集
            rs = pstmt.executeQuery();
            // 4.封装结果集
            List<E> list = new ArrayList<E>();
            while (rs.next()) {
                // 实例化要封装的实体类对象
                E obj = (E) entityClass.newInstance();

                // 取出结果集的元信息:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                // 取出总列数
                int columnCount = rsmd.getColumnCount();
                // 遍历总列数
                for (int i = 1; i <= columnCount; i++) {
                    // 获取每列的名称,列名的序号是从1开始的
                    String columnName = rsmd.getColumnName(i);
                    // 根据得到列名,获取每列的值
                    Object columnValue = rs.getObject(columnName);
                    // 给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
                    // 要求:实体类的属性和数据库表的列名保持一种
                    PropertyDescriptor pd = new PropertyDescriptor(columnName, entityClass);
                    // 获取它的写入方法
                    Method writeMethod = pd.getWriteMethod();
                    // 把获取的列的值,给对象赋值
                    writeMethod.invoke(obj, columnValue);
                }
                // 把赋好值的对象加入到集合中
                list.add(obj);
            }

            this.transaction.commit();

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

            try {
                this.transaction.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            release(pstmt, rs);
        }
        return null;
    }

    private void release(PreparedStatement pstmt, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

12. SqlSession接口

SqlSession接口,其中getMapper方法用来创建dao接口的代理对象,在不改变源码前提下对已有方法进行增强

package com.wenx.demo.basis.mybatis.session;

/**
 * 数据库连接会话,可创建dao接口的代理对象
 *
 * @author Wenx
 * @date 2021/3/17
 */
public interface SqlSession {

    /**
     * 创建dao接口的代理对象
     *
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);

    /**
     * 释放资源
     */
    void close();
}

13. DefaultSqlSession实现类

DefaultSqlSession类为SqlSession接口的实现。用来创建dao接口的代理对象,使用动态代理方式通过MapperProxy类来为持久层接口与mapper映射文件建立关联,调用接口方法实际获取了mapper映射文件的SQL来执行

package com.wenx.demo.basis.mybatis.session.defaults;

import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.MapperProxy;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.utils.Executor;

import java.lang.reflect.Proxy;

/**
 * SqlSession接口实现
 *
 * @author Wenx
 * @date 2021/3/17
 */
public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    /**
     * 创建dao接口的代理对象
     *
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        return (T) Proxy.newProxyInstance(
                daoInterfaceClass.getClassLoader(),
                new Class[]{daoInterfaceClass},
                new MapperProxy(configuration.getMappers(), this.executor));
    }

    /**
     * 释放资源
     */
    @Override
    public void close() {
        if (executor != null) {
            try {
                executor.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

14. MapperProxy代理

MapperProxy类内部主要是个invoke增强方法,用于对dao接口方法进行增强,调用接口时直接获取对应mapper映射文件的SQL语句来执行

package com.wenx.demo.basis.mybatis.session;

import com.wenx.demo.basis.mybatis.utils.Executor;

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

/**
 * @author Wenx
 * @date 2021/3/17
 */
public class MapperProxy implements InvocationHandler {
    /**
     * map的key是dao的全限定类名+方法名
     */
    private final Map<String, MappedStatement> mappers;
    private final Executor executor;

    public MapperProxy(Map<String, MappedStatement> mappers, Executor executor) {
        this.mappers = mappers;
        this.executor = executor;
    }

    /**
     * 用于对dao方法进行增强,实际就是与mapper映射文件做了关联,获取了mapper映射文件的SQL来执行
     *
     * @param proxy  类加载器
     * @param method dao接口方法
     * @param args   参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1.获取方法名
        String methodName = method.getName();
        // 2.获取方法所在类的名称
        String className = method.getDeclaringClass().getName();
        // 3.组合key
        String key = className + "." + methodName;
        // 4.获取mappers中的MappedStatement对象
        MappedStatement mapper = this.mappers.get(key);
        // 5.判断是否有MappedStatement
        if (mapper == null) {
            throw new IllegalArgumentException("参数不合法");
        }
        // 6.调用工具类执行SQL语句
        return this.executor.query(mapper);
    }
}

总结: 所有代码撸完一运行,好使~ 脱离MyBatis包也可以用,说明我们对MyBatis原理剖析的没错,跟着我做一遍之后是不是你对MyBatis的理解又加深了呢?那就是我分享的动力,感谢大家能看到文章最底下~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值