淺析MyBatis(二):手寫一個本身的MyBatis簡單框架

🔗上一篇文章中,咱們由一個快速案例剖析了 MyBatis 的總體架構與總體運行流程,在本篇文章中筆者會根據 MyBatis 的運行流程手寫一個自定義 MyBatis 簡單框架,在實踐中加深對 MyBatis 框架運行流程的理解。本文涉及到的項目代碼能夠在 GitHub 上下載: 🔗my-mybatishtml

話很少說,如今開始!🔛🔛🔛java

1. MyBatis 運行流程回顧

首先經過下面的流程結構圖回顧 MyBatis 的運行流程。在 MyBatis 框架中涉及到的幾個重要的環節包括配置文件的解析、 SqlSessionFactory 和 SqlSession 的建立、 Mapper 接口代理對象的建立以及具體方法的執行。mysql

經過回顧 MyBatis 的運行流程,咱們能夠看到涉及到的 MyBatis 的核心類包括 Resources、Configuration、 XMLConfigBuilder 、 SqlSessionFactory 、 SqlSession 、 MapperProxy 以及 Executor 等等。所以爲了手寫本身的 MyBatis 框架,須要去實現這些運行流程中的核心類。git

2. 手寫一個MyBatis 框架

本節中仍然是以學生表單爲例,會手寫一個 MyBatis 框架,並利用該框架實如今 xml 以及註解兩種不一樣配置方式下查詢學生表單中全部學生信息的操做。學生表的 sql 語句以下所示:github

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() 方法用於查找學生表中的全部學生信息。sql

下面準備自定義 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 語句和輸出結果類型相關的變量便可。代碼以下:session

// 自定義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>

運行測試方法獲得的結果以下所示,驗證了手寫框架的正確性。

image-20210313011156925

此外,咱們修改 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();

再次運行測試方法能夠獲得相同的運行結果,以下圖所示。

image-20210313011648055

經過運行測試方法驗證了本文手寫的 MyBatis 簡單框架的正確性。

4. 全文總結

本文根據原生 MyBatis 框架的運行流程,主要藉助 dom4j 以及 jaxen 工具,逐步實現了一個自定義的 MyBatis 簡易框架,實現案例中查詢全部學生信息的功能。本文的實現過程相對簡單,僅僅只是涉及到了 select 類型的 SQL 語句的解析,不涉及其它查詢類型,也不涉及到 SQL 語句帶參數的狀況,同時也沒法作到對配置文件中與數據庫相關的緩存、事務等相關標籤的解析,總而言之只是一個玩具級別的框架。然而,本文實現這樣一個簡單的自定義 MyBatis 框架的目的是加深對 MyBatis 框架運行流程的理解。所謂萬丈高樓平地起,只有先打牢底層基礎,才能進一步去實現更高級的功能,讀者能夠自行嘗試。

參考資料

淺析MyBatis(一):由一個快速案例剖析MyBatis的總體架構與運行流程

dom4j 官方文檔:https://dom4j.github.io/

jaxen 代碼倉庫:https://github.com/jaxen-xpath/jaxen

《互聯網輕量級 SSM 框架解密:Spring 、 Spring MVC 、 MyBatis 源碼深度剖析》

以爲有用的話,就點個推薦吧~ 🔚🔚🔚

相關文章
相關標籤/搜索