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

Chiakiiii發表於2021-03-16

?上一篇文章中,我們由一個快速案例剖析了 MyBatis 的整體架構與整體執行流程,在本篇文章中筆者會根據 MyBatis 的執行流程手寫一個自定義 MyBatis 簡單框架,在實踐中加深對 MyBatis 框架執行流程的理解。本文涉及到的專案程式碼可以在 GitHub 上下載: ?my-mybatis

話不多說,現在開始!???

1. MyBatis 執行流程回顧

首先通過下面的流程結構圖回顧 MyBatis 的執行流程。在 MyBatis 框架中涉及到的幾個重要的環節包括配置檔案的解析、 SqlSessionFactory 和 SqlSession 的建立、 Mapper 介面代理物件的建立以及具體方法的執行。

通過回顧 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>

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

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 原始碼深度剖析》

覺得有用的話,就點個推薦吧~
???

相關文章