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


🔗上一篇文章中,我們由一個快速案例剖析了 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 源碼深度剖析》

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM