浅析MyBatis(二):手写框架

#0.引言

浅析MyBatis(一):框架与流程 中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBatis 框架运行流程的理解。本文涉及到的项目代码可以在 GitHub 上下载: my-mybatis

话不多说,现在开始!🔛🔛🔛

#1. MyBatis 运行流程回顾

首先通过下面的流程结构图回顾 MyBatis 的运行流程。在 MyBatis 框架中涉及到的几个重要的环节包括配置文件的解析、 SqlSessionFactory 和 SqlSession 的创建、 Mapper 接口代理对象的创建以及具体方法的执行。

execute-diagrams.png

通过回顾 MyBatis 的运行流程,我们可以看到涉及到的 MyBatis 的核心类包括 Resources、Configuration、 XMLConfigBuilder 、 SqlSessionFactory 、 SqlSession 、 MapperProxy 以及 Executor 等等。因此为了手写自己的 MyBatis 框架,需要去实现这些运行流程中的核心类。

#2. 手写一个MyBatis 框架

本节中仍然是以学生表单为例,会手写一个 MyBatis 框架,并利用该框架实现在 xml 以及注解两种不同配置方式下查询学生表单中所有学生信息的操作。学生表的 sql 语句如下所示:

CREATE TABLE `student` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '学生ID',
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `sex` varchar(20) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

insert  into `student`(`id`,`name`,`sex`) values 
(1,'张三','男'),
(2,'托尼·李四','男'),
(3,'王五','女'),
(4,'赵六','男');

学生表对应的 Student 实体类以及 StudentMapper 类可在项目的 entity 包和 mapper 包中查看,我们在 StudentMapper 只定义了 findAll() 方法用于查找学生表中的所有学生信息。

下面准备自定义 MyBatis 框架的配置文件,在 mapper 配置时我们先将配置方式设置为指定 xml 配置文件的方式,整个配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <!-- 配置环境-->
  <environments default="development">
    <!-- 配置MySQL的环境-->
    <environment id="development">
      <!--  配置事务类型-->
      <transactionManager type="JDBC"/>
      <!--  配置数据源-->
      <dataSource type="POOLED">
        <!-- 配置连接数据库的四个基本信息-->
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/>
        <property name="username" value="root"/>
        <property name="password" value="admin"/>
      </dataSource>
    </environment>
  </environments>

  <!-- 指定映射配置文件的位置,映射配置文件的时每个dao独立的配置文件-->
  <mappers>
    <!-- 使用xml配置文件的方式:resource标签 -->
    <mapper resource="mapper/StudentMapper.xml"/>
    <!-- 使用注解方式:class标签 -->
    <!--<mapper class="cn.chiaki.mapper.StudentMapper"/>-->
  </mappers>
</configuration>

本文在编写配置文件时仍按照真正 MyBatis 框架的配置方式进行,这里无需加入配置文件的头信息,同时将数据库的相关信息直接写在配置文件中以简化我们的解析流程。

#2.1 读取和解析配置文件并设置Configuration对象
#2.1.1 自定义Resources类读取MyBatis配置文件

在真正的 MyBatis 框架中对 Java 的原生反射机制进行了相应的封装得到了 ClassLoaderWrapper 这样一个封装类,以此实现更简洁的调用。本文在自定义时就直接采用原生的 Java 反射机制来获取配置文件并转换为输入流。自定义的 Resources 类如下所示:

// 自定义Resources获取配置转换为输入流
public class Resources {

  /**
   * 获取配置文件并转换为输入流
   * @param filePath 配置文件路径
   * @return 配置文件输入流
   */
  public static InputStream getResourcesAsStream(String filePath) {
    return Resources.class.getClassLoader().getResourceAsStream(filePath);
  }
}
#2.1.2 自定义MappedStatement类

在真正的 MyBatis 框架中, MappedStatement 是一个封装了包括 SQL语句、输入参数、输出结果类型等在内的操作数据库配置信息的类。因此本小节中也需要自定义这样一个类,在本文的案例中只需要定义与 SQL 语句和输出结果类型相关的变量即可。代码如下:

// 自定义MappedStatement类
@Data
public class MappedStatement {
  /**  SQL语句  **/
  private String queryString;
  /**  结果类型  **/
  private String resultType;
}
#2.1.3 自定义Configuration类

上一篇文章中已经介绍过,在 MyBatis 框架中对于配置文件的解析都会设置到 Configuration 对象中,然后根据该对象去构建 SqlSessionFactory 以及 SqlSession 等对象,因此 Configuration 是一个关键的类。在本节开头中自定义的配置文件中,真正重要的配置对象就是与数据库连接的标签以及 mapper 配置对应标签下的内容,因此在 Configuration 对象中必须包含与这些内容相关的变量,如下所示:

// 自定义Configuration配置类
@Data
public class Configuration {
  /**  数据库驱动  **/
  private String driver;
  /**  数据库url  **/
  private String url;
  /**  用户名  **/
  private String username;
  /**  密码  **/
  private String password;
  /**  mappers集合  **/
  private Map<String, MappedStatement> mappers = new HashMap<>();
}
#2.1.4 自定义DataSourceUtil工具类获取数据库连接

这里定义一个工具类用于根据 Configuration 对象中与数据库连接有关的属性获取数据库连接的类,编写 getConnection() 方法,如下所示:

// 获取数据库连接的工具类
public class DataSourceUtil {
  public static Connection getConnection(Configuration configuration) {
    try {
      Class.forName(configuration.getDriver());
      return DriverManager.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
#2.1.5 自定义XMLConfigBuilder类解析框架配置文件

进一步自定义解析配置文件的 XMLConfigBuilder 类,根据真正 MyBatis 框架解析配置文件的流程,这个自定义的 XMLConfigBuilder 类应该具备解析 mybatis-config.xml 配置文件的标签信息并设置到 Configuration 对象中的功能。对于 xml 文件的解析,本文采用 dom4j + jaxen 来实现,首先需要在项目的 pom.xml 文件中引入相关依赖。如下所示:

<dependency>
  <groupId>dom4j</groupId>
  <artifactId>dom4j</artifactId>
  <version>1.6.1</version>
</dependency>
<dependency>
  <groupId>jaxen</groupId>
  <artifactId>jaxen</artifactId>
  <version>1.2.0</version>
</dependency>

引入依赖后,我们在 XMLConfigBuilder 类中定义 parse() 方法来解析配置文件并返回 Configuration 对象,如下所示:

public static Configuration parse(InputStream in) {
  try {
    Configuration configuration = new Configuration();
    // 获取SAXReader对象
    SAXReader reader = new SAXReader();
    // 根据输入流获取Document对象
    Document document = reader.read(in);
    // 获取根节点
    Element root = document.getRootElement();
    // 获取所有property节点
    List<Element> propertyElements = root.selectNodes("//property");
    // 遍历节点进行解析并设置到Configuration对象
    for(Element propertyElement : propertyElements){
      String name = propertyElement.attributeValue("name");
      if("driver".equals(name)){
        String driver = propertyElement.attributeValue("value");
        configuration.setDriver(driver);
      }
      if("url".equals(name)){
        String url = propertyElement.attributeValue("value");
        configuration.setUrl(url);
      }
      if("username".equals(name)){
        String username = propertyElement.attributeValue("value");
        configuration.setUsername(username);
      }
      if("password".equals(name)){
        String password = propertyElement.attributeValue("value");
        configuration.setPassword(password);
      }
    }
    // 取出所有mapper标签判断其配置方式
    // 这里只简单配置resource与class两种,分别表示xml配置以及注解配置
    List<Element> mapperElements = root.selectNodes("//mappers/mapper");
    // 遍历集合
    for (Element mapperElement : mapperElements) {
      // 获得resource标签下的内容
      Attribute resourceAttribute = mapperElement.attribute("resource");
      // 如果resource标签下内容不为空则解析xml文件
      if (resourceAttribute != null) {
        String mapperXMLPath = resourceAttribute.getValue();
        // 获取xml路径解析SQL并封装成mappers
        Map<String, MappedStatement> mappers = parseMapperConfiguration(mapperXMLPath);
        // 设置Configuration
        configuration.setMappers(mappers);
      }
      // 获得class标签下的内容
      Attribute classAttribute = mapperElement.attribute("class");
      // 如果class标签下内容不为空则解析注解
      if (classAttribute != null) {
        String mapperClassPath = classAttribute.getValue();
        // 解析注解对应的SQL封装成mappers
        Map<String, MappedStatement> mappers = parseMapperAnnotation(mapperClassPath);
        // 设置Configuration
        configuration.setMappers(mappers);
      }
    }
    //返回Configuration
    return configuration;
  } catch (Exception e) {
    throw new RuntimeException(e);
  } finally {
    try {
      in.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

可以看到在 XMLConfigBuilder#parse() 方法中对 xml 配置文件中与数据库连接相关的属性进行了解析并设置到 Configuration 对象,同时最重要的是对 mapper 标签下的配置方式也进行了解析,并且针对指定 xml 配置文件以及注解的两种情况分别调用了 parseMapperConfiguration() 方法和 parseMapperAnnotation() 两个不同的方法。

  • 2.1.5.1 实现parseMapperConfiguration()方法解析xml配置

针对 xml 配置文件,实现 XMLConfigBuilder#parseMapperConfiguration() 方法来进行解析,如下所示:

/**
 * 根据指定的xml文件路径解析对应的SQL语句并封装成mappers集合
 * @param mapperXMLPath xml配置文件的路径
 * @return 封装完成的mappers集合
 * @throws IOException IO异常
 */
private static Map<String, MappedStatement> parseMapperConfiguration(String mapperXMLPath) throws IOException {
  InputStream in = null;
  try {
    // key值由mapper接口的全限定类名与方法名组成
    // value值是要执行的SQL语句以及实体类的全限定类名
    Map<String, MappedStatement> mappers = new HashMap<>();
    // 获取输入流并根据输入流获取Document节点
    in = Resources.getResourcesAsStream(mapperXMLPath);
    SAXReader saxReader = new SAXReader();
    Document document = saxReader.read(in);
    // 获取根节点以及namespace属性取值
    Element root = document.getRootElement();
    String namespace = root.attributeValue("namespace");
    // 这里只针对SELECT做处理(其它SQL类型同理)
    // 获取所有的select节点
    List<Element> selectElements = root.selectNodes("//select");
    // 遍历select节点集合解析内容并填充mappers集合
    for (Element selectElement : selectElements){
      String id = selectElement.attributeValue("id");
      String resultType = selectElement.attributeValue("resultType");
      String queryString = selectElement.getText();
      String key = namespace + "." + id;
      MappedStatement mappedStatement = new MappedStatement();
      mappedStatement.setQueryString(queryString);
      mappedStatement.setResultType(resultType);
      mappers.put(key, mappedStatement);
    }
    return mappers;
  } catch (Exception e){
    throw new RuntimeException(e);
  } finally {
    // 释放资源
    if (in != null) {
      in.close();
    }
  }
}

在实现 parseMapperConfiguration() 方法时,仍然是利用 dom4j + jaxen 对 Mapper 接口的 xml 配置文件进行解析,遍历 selectElements 集合,获取 namespace 标签以及 id 标签下的内容进行拼接组成 mappers 集合的 key 值,获取 SQL 语句的类型标签(select)以及具体的 SQL 语句封装成 MappedStatement 对象作为 mappers 集合的 value 值,最后返回 mappers 对象。

  • 2.1.5.2 实现parseMapperAnnotation()方法解析注解配置

要实现对注解的解析,首先必须要定义注解,这里针对本案例的查询语句,实现一个 Select 注解,如下所示。

// 自定义Select注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    String value();
}

然后就是实现 parseMapperAnnotation() 对 Select 注解的解析,实现代码如下。

/**
 * 解析mapper接口上的注解并封装成mappers集合
 * @param mapperClassPath mapper接口全限定类名
 * @return 封装完成的mappers集合
 * @throws IOException IO异常
 */
private static Map<String, MappedStatement> parseMapperAnnotation(String mapperClassPath) throws Exception{
  Map<String, MappedStatement> mappers = new HashMap<>();
  // 获取mapper接口对应的Class对象
  Class<?> mapperClass = Class.forName(mapperClassPath);
  // 获取mapper接口中的方法
  Method[] methods = mapperClass.getMethods();
  // 遍历方法数组对SELECT注解进行解析
  for (Method method : methods) {
    boolean isAnnotated = method.isAnnotationPresent(Select.class);
    if (isAnnotated) {
      // 创建Mapper对象
      MappedStatement mappedStatement = new MappedStatement();
      // 取出注解的value属性值
      Select selectAnnotation = method.getAnnotation(Select.class);
      String queryString = selectAnnotation.value();
      mappedStatement.setQueryString(queryString);
      // 获取当前方法的返回值及泛型
      Type type = method.getGenericReturnType();
      // 校验泛型
      if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Type[] types = parameterizedType.getActualTypeArguments();
        Class<?> clazz = (Class<?>) types[0];
        String resultType = clazz.getName();
        // 给Mapper赋值
        mappedStatement.setResultType(resultType);
      }
      // 给key赋值
      String methodName = method.getName();
      String className = method.getDeclaringClass().getName();
      String key = className + "." + methodName;
      // 填充mappers
      mappers.put(key, mappedStatement);
    }
  }
  return mappers;
}

在实现 parseMapperAnnotation() 方法时,根据 Mapper 接口的全限定类名利用反射机制获取 Mapper 接口的 Class 对象以及 Method[] 方法数组,然后遍历方法数组其中的注解相关方法并对注解进行解析,最后完成对 mappers 集合的填充并返回。

#2.2 实现创建会话工厂SqlSessionFactory
#2.2.1 自定义SqlSessionFactoryBuilder会话工厂构建者类

在前期准备中,我们围绕 Configuration 类的配置自定义了 Resource 类、 MappedStatement 类以及 XMLConfiguration 类。接下来根据 MyBatis 的执行流程,需要创建一个 SqlSessionFactory 会话工厂类用于创建 SqlSession 。 所谓工欲善其事,必先利其器。因此首先要自定义一个会话工厂的构建者类 SqlSessionFactoryBuilder ,并在类中定义一个 build() 方法,通过调用 build() 方法来创建 SqlSessionFactory 类,如下所示。

// 会话工厂构建者类
public class SqlSessionFactoryBuilder {
  /**
   * 根据参数的字节输入流构建一个SqlSessionFactory工厂
   * @param in 配置文件的输入流
   * @return SqlSessionFactory
   */
  public SqlSessionFactory build(InputStream in) {
    // 解析配置文件并设置Configuration对象
    Configuration configuration = XMLConfigBuilder.parse(in);
    // 根据Configuration对象构建会话工厂
    return new DefaultSqlSessionFactory(configuration);
  }
}

在这个类中我们定义了 build() 方法,入参是 MyBatis 配置文件的输入流,首先会调用 XMLConfigBuilder#parse() 方法对配置文件输入流进行解析并设置 Configuration 对象,然后会根据 Configuration 对象构建一个 DefaultSqlSessionFactory 对象并返回。上篇文章中已经介绍了在 MyBatis 中 SqlSessionFactory 接口有 DefaultSqlSessionFactory 这样一个默认实现类。因此本文也定义 DefaultSqlSessionFactory 这样一个默认实现类。

#2.2.2 自定义SqlSessionFactory接口与其默认实现类

会话工厂类 SqlSessionFactory 是一个接口,其中定义了一个 openSession() 方法用于创建 SqlSession 会话,如下所示:

// 自定义SqlSessionFactory接口
public interface SqlSessionFactory {
  /**
   * 用于打开一个新的SqlSession对象
   * @return SqlSession
   */
  SqlSession openSession();
}

该接口有一个 DefaultSqlSessionFactory 默认实现类,其中实现了 openSession() 方法,如下所示:

// 自定义DefaultSqlSessionFactory默认实现类
public class DefaultSqlSessionFactory implements SqlSessionFactory {
  // Configuration对象
  private final Configuration configuration;
  // 构造方法
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
  /**
   * 用于创建一个新的操作数据库对象
   * @return SqlSession
   */
  @Override
  public SqlSession openSession() {
    return new DefaultSqlSession(configuration);
  }
}

可以看到在实现 openSession() 方法中涉及到了 SqlSession 接口以及 SqlSession 接口的 DefaultSqlSession 默认实现类。

#2.3 实现创建会话SqlSession
#2.3.1 自定义SqlSession接口与其默认实现类

在自定义 SqlSession 接口时,先思考该接口中需要定义哪些方法。在 MyBatis 执行流程中,需要使用 SqlSession 来创建一个 Mapper 接口的代理实例,因此一定需要有 getMapper() 方法来创建 MapperProxy 代理实例。同时,还会涉及到 SqlSession 的释放资源的操作,因此 close() 方法也是必不可少的。因此自定义 SqlSession 的代码如下:

// 自定义SqlSession接口
public interface SqlSession {
  
  /**
   * 根据参数创建一个代理对象
   * @param mapperInterfaceClass mapper接口的Class对象
   * @param <T> 泛型
   * @return mapper接口的代理实例
   */
  <T> T getMapper(Class<T> mapperInterfaceClass);
  
  /**
   * 释放资源
   */
  void close();
}

进一步创建 SqlSession 接口的 DefaultSqlSession 默认实现类,并实现接口中的 getMapper() 和 close() 方法。

public class DefaultSqlSession implements SqlSession {
  
  // 定义成员变量
  private final Configuration configuration;
  private final Connection connection;
  
  // 构造方法
  public DefaultSqlSession(Configuration configuration) {
    this.configuration = configuration;
    // 调用工具类获取数据库连接
    connection = DataSourceUtil.getConnection(configuration);
  }
  
  /**
   * 用于创建代理对象
   * @param mapperInterfaceClass mapper接口的Class对象
   * @param <T> 泛型
   * @return mapper接口的代理对象
   */
  @Override
  public <T> T getMapper(Class<T> mapperInterfaceClass) {
    // 动态代理
    return (T) Proxy.newProxyInstance(mapperInterfaceClass.getClassLoader(), 
                                      new Class[]{mapperInterfaceClass}, 
                                      new MapperProxyFactory(configuration.getMappers(), connection));
  }

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

与真正的 MyBatis 实现流程一样,本文在 getMapper() 方法的实现过程中也采用动态代理的方式返回 Mapper 接口的代理实例,其中包括了构建 MapperProxyFactory 类。在调用 Proxy#newProxyInstance() 方法时,包括的入参以及含义如下:

  • ClassLoader :和被代理对象使用相同的类加载器,这里就是 mapperInterfaceClass 的 ClassLoader ;
  • Class[] :代理对象和被代理对象要有相同的行为(方法);
  • InvocationHandler : 事情处理,执行目标对象的方法时会触发事情处理器方法,把当前执行的目标对象方作为参数传入。

然后 DefaultSqlSession#close() 方法的实现主要就是调用数据库连接的 close() 方法。

#2.3.2 自定义MapperProxyFactory类

为了实现动态代理,需要自定义 MapperProxyFactory 类用于创建 Mapper 接口的代理实例,其代码如下:

// 自定义MapperProxyFactory类
public class MapperProxyFactory implements InvocationHandler {
  // mappers集合
  private final Map<String, MappedStatement> mappers;
  private final Connection connection;
  
  public MapperProxyFactory(Map<String, MappedStatement> mappers, Connection connection) {
    this.mappers = mappers;
    this.connection = connection;
  }

  // 实现InvocationHandler接口的invoke()方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取方法名
    String methodName = method.getName();
    // 获取方法所在类的名称
    String className = method.getDeclaringClass().getName();
    // 组合key
    String key = className + "." + methodName;
    // 获取mappers中的Mapper对象
    MappedStatement mappedStatement = mappers.get(key);
    // 判断是否有mapper
    if (mappedStatement != null) {
      // 调用Executor()工具类的query()方法
      return new Executor().query(mappedStatement, connection);
    } else {
      throw new IllegalArgumentException("传入参数有误");
    }
  }
}
#2.4 执行代理对象的相关方法

创建 Mapper 接口的代理对象后,下一步就是执行代理对象的相关方法,这里需要实现 Executor 类用于执行 MapperedStatement 对象中的封装的 SQL 语句并返回其中指定输出类型的结果, 在 Executor 类中定义查询所有相关的 selectList() 方法,如下所示:

// 自定义Executor类
public class Executor {
  
  // query()方法将selectList()的返回结果转换为Object类型
  public Object query(MappedStatement mappedStatement, Connection connection) {
    return selectList(mappedStatement, connection);
  }

  /**
   * selectList()方法
   * @param mappedStatement mapper接口
   * @param connection 数据库连接
   * @param <T> 泛型
   * @return 结果
   */
  public <T> List<T> selectList(MappedStatement mappedStatement, Connection connection) {
    
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    
    try {
      // 取出SQL语句
      String queryString = mappedStatement.getQueryString();
      // 取出结果类型
      String resultType = mappedStatement.getResultType();
      Class<?> clazz = Class.forName(resultType);
      // 获取PreparedStatement对象并执行
      preparedStatement = connection.prepareStatement(queryString);
      resultSet = preparedStatement.executeQuery();
      // 从结果集对象封装结果
      List<T> list = new ArrayList<>();
      while(resultSet.next()) {
        //实例化要封装的实体类对象
        T obj = (T) clazz.getDeclaredConstructor().newInstance();
        // 取出结果集的元信息
        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
        // 取出总列数
        int columnCount = resultSetMetaData.getColumnCount();
        // 遍历总列数给对象赋值
        for (int i = 1; i <= columnCount; i++) {
          String columnName = resultSetMetaData.getColumnName(i);
          Object columnValue = resultSet.getObject(columnName);
          PropertyDescriptor descriptor = new PropertyDescriptor(columnName, clazz);
          Method writeMethod = descriptor.getWriteMethod();
          writeMethod.invoke(obj, columnValue);
        }
        // 把赋好值的对象加入到集合中
        list.add(obj);
      }
      return list;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      // 调用release()方法释放资源
      release(preparedStatement, resultSet);
    }
  }

  /**
   * 释放资源
   * @param preparedStatement preparedStatement对象
   * @param resultSet resultSet对象
   */
  private void release(PreparedStatement preparedStatement, ResultSet resultSet) {
    if (resultSet != null) {
      try {
        resultSet.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    if (preparedStatement != null) {
      try {
        preparedStatement.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

在 Executor 类中最为核心的就是 selectList() 方法,该方法的实现逻辑在于从 MappedStatement 对象中取出 SQL 语句以及结果集类型,然后根据 SQL 语句信息构建 PreparedStatement 对象并执行返回 ResultSet 对象,然后将 ResultSet 中的数据转换为 MappedStatement 中指定的结果集类型 ResultType 的数据并返回。

#2.5 小结

至此,一个手写 MyBatis 简单框架就搭建完成了,其搭建过程完全遵循原生 MyBatis 框架对 SQL 语句的执行流程,现对上述过程做下小结:

  • ✅编写必要的实体类,包括 Configuration 、 MapperStatement 类等;
  • ✅编写必要的工具类,包括获取数据库连接的 DataSourceUtil 类、读取配置文件的 Resources 类以及解析配置的 XMLConfigBuilder 类;
  • ✅编写 XMLConfigBuilder 类时,基于 dom4j + jaxen 对 xml 配置文件进行加载和解析,基于反射机制对自定义注解配置进行加载和解析,加载解析完成后填充 mappers 集合并设置到 Configuration 对象中;
  • ✅编写 SqlSessionFactoryBuilder 构建者类用于构建 SqlSessionFactory 类;
  • ✅编写 SqlSessionFactory 和 SqlSession 接口及其默认实现类;
  • ✅编写 MapperProxyFactory 类实现基于动态代理创建 Mapper 接口的代理实例;
  • ✅编写 Executor 类用于根据 mappers 集合执行相应 SQL 语句并返回结果。

#3. 自定义MyBatis框架的测试

为了测试前文中手写的 MyBatis 简单框架,定义如下的测试方法:

// MyBatisTest测试类
public class MybatisTest {
  
  private InputStream in;
  private SqlSession sqlSession;
  
  @Before
  public void init() {
    // 读取MyBatis的配置文件
    in = Resources.getResourcesAsStream("mybatis-config.xml");
    // 创建SqlSessionFactory的构建者对象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    // 使用builder创建SqlSessionFactory对象
    SqlSessionFactory factory = builder.build(in);
    // 使用factory创建sqlSession对象
    sqlSession = factory.openSession();
  }

  @Test
  public void testMyMybatis() {
    // 使用SqlSession创建Mapper接口的代理对象
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    // 使用代理对象执行方法
    List<Student> students = studentMapper.findAll();
    System.out.println(students);
  }

  @After
  public void close() throws IOException {
    // 关闭资源
    sqlSession.close();
    in.close();
  }
}

首先在配置文件中将 mapper 的配置方式设置为指定 xml 文件,其中 StudentMapper 接口的 xml 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="cn.chiaki.mapper.StudentMapper">
  <select id="findAll" resultType="cn.chiaki.entity.Student">
    SELECT * FROM student
  </select>
</mapper>

运行测试方法得到的结果如下所示,验证了手写框架的正确性。

test-demo.png

此外,我们修改 mybatis-config.xml 配置文件的 mapper 配置方式为注解配置,同时在 StudentMapper 接口上加入注解,如下所示。

<mappers>
  <!-- 使用xml配置文件的方式:resource标签 -->
  <!--<mapper resource="mapper/StudentMapper.xml"/>-->
  <!-- 使用注解方式:class标签 -->
  <mapper class="cn.chiaki.mapper.StudentMapper"/>
</mappers>
@Select("SELECT * FROM STUDENT")
List<Student> findAll();

再次运行测试方法可以得到相同的运行结果,如下图所示。

test-demo-1.png

通过运行测试方法验证了本文手写的 MyBatis 简单框架的正确性。

#4. 全文总结

本文根据原生 MyBatis 框架的运行流程,主要借助 dom4j 以及 jaxen 工具,逐步实现了一个自定义的 MyBatis 简易框架,实现案例中查询所有学生信息的功能。本文的实现过程相对简单,仅仅只是涉及到了 select 类型的 SQL 语句的解析,不涉及其它查询类型,也不涉及到 SQL 语句带参数的情况,同时也无法做到对配置文件中与数据库相关的缓存、事务等相关标签的解析,总而言之只是一个玩具级别的框架。然而,本文实现这样一个简单的自定义 MyBatis 框架的目的是加深对 MyBatis 框架运行流程的理解。所谓万丈高楼平地起,只有先打牢底层基础,才能进一步去实现更高级的功能,读者可以自行尝试。

#参考资料

浅析MyBatis(一):框架与流程

dom4j 官方文档

jaxen 代码仓库

《互联网轻量级 SSM 框架解密:Spring 、 Spring MVC 、 MyBatis 源码深度剖析》