Mybatis執行流程學習之手寫mybatis雛形

theMine發表於2021-01-22

Mybatis是目前開發中最常用的一款基於ORM思想的半自動持久層框架,平時我們都僅僅停留在使用階段,對mybatis是怎樣執行的並不清楚,今天抽空找到一些資料自學了一波,自己寫了一個mybatis的雛形,在此對學習過程做一個記錄
首先,我們新建一個提供mybatis框架功能的工程IMybatis,這個工程中主要完成mybatis整個初始化和執行過程的功能開發。

 

該工程中用到的依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.my</groupId>
    <artifactId>IMybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compile.encoding>UTF-8</maven.compile.encoding>
        <java.version>1.8</java.version>
        <maven.compile.source>1.8</maven.compile.source>
        <maven.compile.target>1.8</maven.compile.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.26</version>
        </dependency>
    </dependencies>

</project>

 

我們在完成上面第一步中框架的編寫後會進行打包釋出到本地倉庫,再新建一個測試工程IMybatis-test,這個工程的pom檔案中會引入IMybatis工程的依賴,完成測試

 

 

 該工程的依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.my</groupId>
    <artifactId>IMybatis-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.my</groupId>
            <artifactId>IMybatis</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

 

mybatis要完成對資料庫的連線,增刪改查功能,需要有兩個配置檔案(這裡先不管以註解的形式在mapper介面中編寫的sql),一個是配置的資料庫的連線資訊,我這裡是datasourceConfig.xml,

<configuration>
    <!-- 資料庫配置資訊 -->
    <dataSource>
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql:///test?serverTimezone=Asia/Shanghai"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </dataSource>

    <mapper resource="UserMapper.xml"></mapper>
</configuration>

 

另一個是提供sql的mapper檔案,這裡是UserMapper.xml,這兩個檔案都在IMybatis-test工程中提供

<mapper namespace="com.my.dao.UserMapper">

    <!-- sql的唯一表示由 namespace.id 來組成statementId -->
    <select id="findAll" resultType="com.my.pojo.User">
      select * from user
    </select>

    <select id="findOne" parameterType="com.my.pojo.User" resultType="com.my.pojo.User">
        select * from user where id = #{id}
    </select>

    <select id="findById" parameterType="java.lang.Long" resultType="com.my.pojo.User">
        select * from user where id = #{id}
    </select>

    <delete id="delete" parameterType="com.my.pojo.User">
        delete from user where id = #{id}
    </delete>

    <delete id="deleteById" parameterType="java.lang.Long">
        delete from user where id = #{id}
    </delete>

    <update id="update" parameterType="com.my.pojo.User">
        update user set name = #{name} where id = #{id}
    </update>

    <insert id="insert" parameterType="com.my.pojo.User">
        insert into user(id, name) VALUES(#{id}, #{name})
    </insert>
</mapper>

 

下面就要完成IMybatis的功能開發。

一、新建Resource類完成對datasourceConfig.xml檔案的載入,將其以流的形式載入到記憶體中

package com.my.io;

import java.io.InputStream;

/**
 * @Description: 配置檔案讀取
 * @Author lzh
 * @Date 2020/12/6 16:01
 */
public class Resource {

  /**
   * 根據傳遞的路徑path去讀取到該路徑下的配置檔案datasourceConfig.xml,並將其讀成位元組流返回
   * @param path
   * @return InputStream
   */
  public static InputStream getResourceAsStream(String path){
    InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
    return resourceAsStream;
  }
}

二、新建SqlSessionFactoryBuilder類,編寫build()方法,一步一步構建SqlSessionFactory物件

package com.my.sqlSession;

import com.my.config.XMLConfigBuilder;
import com.my.pojo.Configuration;

import java.io.InputStream;

/**
 * @Description: 解析配置檔案
 * @Author lzh
 * @Date 2020/12/6 16:23
 */
public class SqlSessionFactoryBuilder {

  /**
   * 根據位元組流解析出配置檔案中各個標籤的值,並封裝到Configuration中,建立DefaultSqlSessionFactory物件
   * @param in
   * @return
   * @throws Exception
   */
  public SqlSessionFactory build(InputStream in) throws Exception {

    //建立一個XMLConfigBuilder物件
    XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();

    //對配置檔案進行解析
    Configuration configuration = xmlConfigBuilder.parseConfig(in);

    //建立DefaultSqlSessionFactory物件
    DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

    return defaultSqlSessionFactory;
  }
}

三、在build方法中可以看到首先要建立一個XMLConfigBuilder 物件,在該物件中編寫了一個parseConfig()方法完成對配置檔案的解析,並完成對Configuration 物件的封裝,Configuration 是我們這個工程中的一個非常核心的物件,裡面儲存了對配置檔案解析後的結果,同樣在真正的Mybatis框架中也有該物件,當然功能比我這裡的更強大。

package com.my.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.my.io.Resource;
import com.my.pojo.Configuration;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

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

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:26
 */
public class XMLConfigBuilder {

  private Configuration configuration;

  public XMLConfigBuilder() {
    this.configuration = new Configuration();
  }

  /**
   * 解析dataSourceConfig.xml
   * @param in
   * @return
   * @throws Exception
   */
  public Configuration parseConfig(InputStream in) throws Exception {

    //利用dom4j技術對配置檔案進行解析
    Document document = new SAXReader().read(in);
    Element rootElement = document.getRootElement();
    //查詢dataSourceConfig.xml中的property標籤
    List<Element> list = rootElement.selectNodes("//property");
    Properties properties = new Properties();
    for (Element element : list) {
      //取出每個property標籤中的值存到Properties物件中
      String name = element.attributeValue("name");
      String value = element.attributeValue("value");
      properties.setProperty(name, value);
    }
    //從Properties中取出各個屬性構建一個連線池,來提供對資料庫連線的管理,避免資源浪費,提高效能
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setDriverClassName(properties.getProperty("driverClass"));
    druidDataSource.setUrl(properties.getProperty("url"));
    druidDataSource.setUsername(properties.getProperty("username"));
    druidDataSource.setPassword(properties.getProperty("password"));
    //將連線池物件放入Configuration物件中
    configuration.setDataSource(druidDataSource);

    //解析dataSourceConfig.xml中的mapper標籤,mapper標籤中的resource屬性值存放的就是UserMapper.xml的檔案位置
    List<Element> mapperList = rootElement.selectNodes("//mapper");
    for (Element element : mapperList) {
      String mapperPath = element.attributeValue("resource");
      InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath);
      //解析UserMapper.xml檔案,進一步封裝Configuration物件
      XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
      xmlMapperBuilder.parse(resourceAsStream);
    }
    return configuration;
  }
}

上圖紅色的地方建立了一個XMLMapperBuilder物件,該物件提供了一個parse()方法,就是完成對UserMapper.xml檔案的解析,並完成對Configuration封裝

package com.my.config;

import com.my.config.eunm.SqlCommandType;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

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

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 17:03
 */
public class XMLMapperBuilder {

  private Configuration configuration;

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

  /**
   * 解析UserMapper.xml配置檔案中得內容,將每一個標籤構建成一個MappedStatement,並賦值到Configuration中
   * @param in
   * @throws DocumentException
   */
  public void parse(InputStream in) throws DocumentException {
    Document document = new SAXReader().read(in);
    Element rootElement = document.getRootElement();
    String namespace = rootElement.attributeValue("namespace");
    //解析select標籤
    List<Element> selectList = rootElement.selectNodes("//select");
    this.parseElement(selectList, namespace, SqlCommandType.SELECT);

    //解析insert標籤
    List<Element> insertList = rootElement.selectNodes("//insert");
    this.parseElement(insertList, namespace, SqlCommandType.INSERT);

    //解析update標籤
    List<Element> updateList = rootElement.selectNodes("//update");
    this.parseElement(updateList, namespace, SqlCommandType.UPDATE);

    //解析delete標籤
    List<Element> deleteList = rootElement.selectNodes("//delete");
    this.parseElement(deleteList, namespace, SqlCommandType.DELETE);
  }

  /**
   * 解析mapper.xml檔案中增刪改查標籤
   * @param elementList
   * @param namespace
   * @param sqlCommandType
   */
  private void parseElement(List<Element> elementList, String namespace, SqlCommandType sqlCommandType) {
    for (Element element : elementList) {
      String id = element.attributeValue("id");
      String resultType = element.attributeValue("resultType");
      String parameterType = element.attributeValue("parameterType");
      String sql = element.getTextTrim();

      MappedStatement mappedStatement = new MappedStatement();
      mappedStatement.setSqlCommandType(sqlCommandType);
      mappedStatement.setId(id);
      mappedStatement.setParameterType(parameterType);
      mappedStatement.setResultType(resultType);
      mappedStatement.setSql(sql);

      configuration.getMappedStatementMap().put(namespace + "." + id, mappedStatement);
    }
  }
}

該類中用到的SqlCommandType是一個列舉類,就是列舉的UserMapper.xml中的幾個主要的sql標籤型別增刪改查,也是借鑑的原Mybatis框架中的寫法

package com.my.config.eunm;

public enum SqlCommandType {
  INSERT,
  UPDATE,
  DELETE,
  SELECT;

  private SqlCommandType(){

  }
}

還有一個MappedStatement物件,這個物件中就是封裝的每一個insert、update、delete、select標籤中的資訊(包括每個標籤中的id、parameterType、resutType、sql語句等等),每個標籤就是一個MappedStatement物件

package com.my.pojo;

import com.my.config.eunm.SqlCommandType;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:17
 */
public class MappedStatement {

  private SqlCommandType sqlCommandType;

  private String id;

  private String resultType;

  private String parameterType;

  private String sql;

  public SqlCommandType getSqlCommandType() {
    return sqlCommandType;
  }

  public void setSqlCommandType(SqlCommandType sqlCommandType) {
    this.sqlCommandType = sqlCommandType;
  }

  public String getId() {
    return id;
  }

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

  public String getResultType() {
    return resultType;
  }

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

  public String getParameterType() {
    return parameterType;
  }

  public void setParameterType(String parameterType) {
    this.parameterType = parameterType;
  }

  public String getSql() {
    return sql;
  }

  public void setSql(String sql) {
    this.sql = sql;
  }
}

封裝好MappedStatement物件後,再將其放入Configuration物件的mappedStatementMap屬性中,該屬性就是一個Map集合,key就是UserMapper.xml檔案中的namespace的值+ "." +每一個標籤的id值(例如我們這裡的com.my.dao.UserMapper.findAll),因為一個Mapper介面對應一個Mapper.xml檔案,而每個Mapper.xml檔案中的namespace的值就是Mapper介面的全限定類名,每個標籤的id值就是Mapper介面中對應的方法名,所以通過這個組合key就能和Mapper介面產生關聯,當我們在呼叫Mapper介面中的方法時,就可以通過Mapper介面的全限定類名和呼叫的方法名在Configuration中的Map集合中找到對應的MappedStatement物件,也就是能拿到需要執行的sql、引數型別、返回值型別等等。

package com.my.pojo;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/** 核心物件
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:18
 */
public class Configuration {

  /**
   * 資料來源
   */
  private DataSource dataSource;

  /**
   * key:statementId vlaue:封裝好的MappedStatement
   */
  private Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();

  public DataSource getDataSource() {
    return dataSource;
  }

  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  public Map<String, MappedStatement> getMappedStatementMap() {
    return mappedStatementMap;
  }

  public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
    this.mappedStatementMap = mappedStatementMap;
  }
}

到這裡我們的Configuration物件就封裝完畢。

四、然後我們可以在第二步中的SqlSessionFactoryBuilder類的build()方法中看到,根據Configuration物件構造出了DefaultSqlSessionFactory工廠物件,整個構建DefaultSqlSessionFactory的過程就是一個構建者模式的體現(通過多個小的物件構建出一個大的物件)

package com.my.sqlSession;

public interface SqlSessionFactory {

  SqlSession createSqlSession();
}

 

package com.my.sqlSession;

import com.my.pojo.Configuration;

/**
 * @Description: SqlSession的工廠物件,用於生產SqlSession
 * @Author lzh
 * @Date 2020/12/6 17:17
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private Configuration configuration;

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

  /**
   * 建立SqlSession會話
   * @return
   */
  public SqlSession createSqlSession() {
    return new DefaultSqlSession(configuration);
  }
}

五、利用DefaultSqlSessionFactory工廠物件 的createSqlSession()方法來獲取一個SqlSession物件,就是一個我們所說的一個會話物件,該物件也是一個非常重要的物件

package com.my.sqlSession;

import java.util.List;

/**
 * @Description: SqlSession
 * @Author lzh
 * @Date 2020/12/6 17:18
 */
public interface SqlSession {

  <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception;

  <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception;

  <T> T getMapper(Class<?> mapperClass);
}
package com.my.sqlSession;

import com.my.config.eunm.SqlCommandType;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;

import java.lang.reflect.*;
import java.util.List;

/**
 * @Description: SqlSession會話的實現
 * @Author lzh
 * @Date 2020/12/6 17:21
 */
public class DefaultSqlSession implements SqlSession, InvocationHandler {

  private Configuration configuration;

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

  /**
   * 多條查詢
   * @param statementId
   * @param param
   * @param <E>
   * @return
   * @throws Exception
   */
  public <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception {

    SimpleExecutor simpleExecutor = new SimpleExecutor();
    List<Object> query = simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param);
    return (List<E>) query;
  }

  /**
   * 單條查詢
   * @param statementId
   * @param param
   * @param <T>
   * @return
   * @throws Exception
   */
  public <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    List<Object> objects = selectList(statementId, methodParameterType, param);
    if (objects.size() == 1){
      return (T) objects.get(0);
    }else if (objects.size() <= 0){
      return null;
    }else {
      throw new RuntimeException("Result more than one");
    }
  }

  /**
   * 新增
   * @param statementId
   * @param param
   * @return
   */
  public int insert(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    return update(statementId, methodParameterType, param);
  }

  /**
   * 修改
   * @param statementId
   * @param param
   * @return
   */
  public int update(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    SimpleExecutor simpleExecutor = new SimpleExecutor();
    return simpleExecutor.update(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param);
  }

  /**
   * 刪除
   * @param statementId
   * @param param
   * @return
   */
  public int delete(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    return update(statementId, methodParameterType, param);
  }

  /**
   * 建立代理物件
   * @param mapperClass
   * @param <T>
   * @return
   */
  @Override
  public <T> T getMapper(Class<?> mapperClass) {
    Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, this);
    return (T) proxyInstance;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    Class<?> methodParameterType = null;
    if (null != method.getParameterTypes() && 0 < method.getParameterTypes().length){
      methodParameterType = method.getParameterTypes()[0];
    }
    String className = method.getDeclaringClass().getName();
    String statementId = className + "." + methodName;
    MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
    if (SqlCommandType.SELECT == sqlCommandType){
      Type genericReturnType = method.getGenericReturnType();
      if (genericReturnType instanceof ParameterizedType){
        return selectList(statementId, methodParameterType, args);
      }
      return selectOne(statementId, methodParameterType, args);
    }else if (SqlCommandType.INSERT == sqlCommandType){
      return insert(statementId, methodParameterType, args);
    }else if (SqlCommandType.UPDATE == sqlCommandType){
      return update(statementId, methodParameterType, args);
    }else if (SqlCommandType.DELETE == sqlCommandType){
      return delete(statementId, methodParameterType, args);
    }else {
      throw new RuntimeException("Unknown SqlCommandType For: " + sqlCommandType);
    }
  }
}

六、在SqlSession中,就提供了增刪改查方法,用於運算元據庫,我們另外還可以看到一個getMapper()方法,該方法需要傳入一個Class引數,那麼這個方法是幹什麼的呢?我們有過開發經驗的朋友都知道很早以前在用spring+Mybatis框架開發的時候,對每一個Dao(也就是這裡我們說的Mapper層)層的介面都會去寫一個實現類DaoImpl,在實現類中通過JDBC來完成對資料庫的操作,這樣的編碼方式會存在很多問題,比如:

  1. 每次執行一個方法都會區獲取一個Connection物件,也就是建立一個資料庫連線
  2. sql語句和業務程式碼融合在一起,增加程式碼耦合度,也不便於維護
  3. 封裝返回結果麻煩,不夠智慧

所以針對第一個問題我們引入了連線池來管理資料庫連線,每次都是從池子裡面去獲取,減少了資源消耗,提高了效率,針對後面兩個問題,首先Mybatis去調了DaoImpl實現類,其次,通過Java反射技術完成對引數的賦值和對返回結果的動態封裝(這一步後面程式碼中會有體現)。那麼去掉了DaoImpl實現類,Dao介面中需要做的事總是需要有人來做的,否則無法完成對資料庫的操作,因此Mybatis中會為每個Dao介面(也就是這裡我們說的Mapper介面)生成一個代理物件,去完成之前DaoImpl做的事。這裡的getMapper()方法就是去獲取傳入引數物件的代理物件,我們這裡就是獲取UserMapper介面的代理物件,建立代理物件時我們可以看到在getMapper()方法中的Proxy.newProxyInstance(),需要傳遞三個引數,第一個引數就是一個類載入器,第二引數就是我們需要為哪個物件產生代理物件,也就是getMapper()方法的引數,重點是第三個引數,需要傳入一個InvocationHandler物件,而InvocationHandler是一個介面,我們這裡的DefaultSqlSession實現了這個介面,所以第三個引數傳的就是this,該類本身。實現了InvocationHandler介面就需要重寫invoke()方法,而我們知道呼叫代理物件的方法,都會走到該invoke()方法中,所以我們這裡呼叫UserMapper介面中的方法時,同樣會執行這裡的invoke方法,這樣在invoke()方法中就可以完成我們以前在DaoImpl中需要完成的事。

七、下面我們具體來看下invoke()中做了什麼,首先看下三個引數,第一個就是一個代理物件,第二個就是我們呼叫的方法Method,第三個就是呼叫方法時傳入的引數args,那麼我們根據Method物件就可以獲取到該方法的全限定類名和該方法的名稱,從而組合一個statemenId,而我們在上面第五步中通過createSqlSession()方法建立SqlSession物件時,是將我們封裝的Configuration物件傳入了,所有這裡我們可以通過statementId在Configuration物件的mappedStatementMap這個Map集合中找到我們封裝的MappedStatement物件,通過MappedStatement物件中的SqlCommandType的值我們可以判斷出我們需要執行增刪改查中的哪個方法,從而去呼叫該類具體的增刪改查方法,在執行具體方法時,我們這裡並沒有在SqlSession物件中直接去運算元據庫,而是將這些crud操作交給了一個SimpleExecutor執行器去完成真正對資料庫的操作。

package com.my.sqlSession;

import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;

import java.util.List;

public interface Executor {

  <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object... param) throws Exception;

  int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception;

}
package com.my.sqlSession;

import com.my.config.BoundSql;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;
import com.my.utils.GenericTokenParser;
import com.my.utils.ParameterMapping;
import com.my.utils.ParameterMappingTokenHandler;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Description: Executor執行器
 * @Author lzh
 * @Date 2020/12/6 17:32
 */
public class SimpleExecutor implements Executor {

  /**
   * 真正的查詢方法,負責完成JDBC的操作
   * @param configuration
   * @param mappedStatement
   * @param param
   * @param <E>
   * @return
   * @throws Exception
   */
  public <E> List<E>  query(Configuration configuration, MappedStatement mappedStatement,Class<?> methodParameterType, Object... param) throws Exception {
    PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param);
    //執行sql,返回結果集ResultSet
    ResultSet resultSet = preparedStatement.executeQuery();
    //對結果封裝,對映出對應得返回型別
    String resultType = mappedStatement.getResultType();
    Class<?> resultClass = getClassType(resultType);
    List<Object> result = new ArrayList<Object>();
    while (resultSet.next()){
      Object o = resultClass.newInstance();
      ResultSetMetaData metaData = resultSet.getMetaData();
      for (int i = 1; i <= metaData.getColumnCount(); i++) {
        String columnName = metaData.getColumnName(i);
        Object object = resultSet.getObject(columnName);
        PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
        Method writeMethod = propertyDescriptor.getWriteMethod();
        writeMethod.invoke(o, object);
      }
      result.add(o);
    }
    return (List<E>) result;
  }

  @Override
  public int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception {
    PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param);
    preparedStatement.execute();
    int row = preparedStatement.getUpdateCount();
    return row;
  }

  /**
   * 獲取PreparedStatement物件
   * @param configuration
   * @param mappedStatement
   * @param param
   * @return
   * @throws Exception
   */
  private PreparedStatement createPreparedStatement(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception {

    //獲取資料庫連線
    Connection connection = configuration.getDataSource().getConnection();
    //從MappedStatement中取出sql,現在的sql就是userMapper.xml中我們編寫的帶有#{}的sql語句
    String sql = mappedStatement.getSql();
    //處理sql語句,解析出sql語句中#{}中的屬性值,並將#{}替換為?,封裝到BoundSql物件中
    BoundSql boundSql = getBoundSql(sql);

    //獲取PreparedStatement物件
    PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
    //如果有引數,給引數賦值
    String parameterType = mappedStatement.getParameterType();
    if (null != parameterType){
      Class<?> parameterClass = getClassType(parameterType);

      List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
      for (int i = 0; i < parameterMappingList.size(); i++) {
        if (isObject(methodParameterType)){
          preparedStatement.setObject(i + 1, param[0]);
        }else {
          ParameterMapping parameterMapping = parameterMappingList.get(i);
          //該content就是我們sql中#{id}中的id
          String content = parameterMapping.getContent();
          //利用反射在parameterClass中取出content這個屬性的值,並完成sql的賦值
          Field declaredField = parameterClass.getDeclaredField(content);
          declaredField.setAccessible(true);
          Object o = declaredField.get(param[0]);
          preparedStatement.setObject(i + 1, o);
        }
      }
    }
    return preparedStatement;
  }

  /**
   * 根據引數型別或者返回值型別獲取該物件的Class
   * @param parameterType
   * @return
   * @throws ClassNotFoundException
   */
  private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
    Class<?> aClass = Class.forName(parameterType);
    return aClass;
  }

  /**
   * 解析sql,封裝成BoundSql
   * @param sql
   * @return
   */
  private BoundSql getBoundSql(String sql) {
    ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
    GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
    //解析出來的sql
    String parseSql = genericTokenParser.parse(sql);
    //解析出來的id和name
    List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
    BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
    return boundSql;
  }

  private Boolean isObject(Class<?> methodParameterType){
    if (null == methodParameterType){
      return false;
    }
    if (Integer.class.getName().equals(methodParameterType.getName())
        || Long.class.getName().equals(methodParameterType.getName())
        || String.class.getName().equals(methodParameterType.getName())
        || Double.class.getName().equals(methodParameterType.getName())
        || Float.class.getName().equals(methodParameterType.getName())
        || Byte.class.getName().equals(methodParameterType.getName())
        || Short.class.getName().equals(methodParameterType.getName())
        || Character.class.getName().equals(methodParameterType.getName())
        || Boolean.class.getName().equals(methodParameterType.getName())
        || Date.class.getName().equals(methodParameterType.getName())){
      return true;
    }
    return false;
  }
}

八、在這個執行器中就是真正完成對資料庫的操作,從連線池中獲取一個Connection連線,從MappedStatement中獲取到要執行的sql,這裡注意這時候的sql還是從UserMapper.xml中解析出來的sql(select * from user where id = #{id}),需要對其進行處理用?替換掉#{},並記錄大括號中的引數,因為JDBC中引數的佔位符是?,所以這裡的getBoundSql()方法就是在做這些事情,最終封裝成一個BoundSql物件。

package com.my.config;

import com.my.utils.ParameterMapping;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: sql
 * @Author lzh
 * @Date 2020/12/6 17:42
 */
public class BoundSql {

  private String sqlText;

  private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>();

  public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
    this.sqlText = sqlText;
    this.parameterMappingList = parameterMappingList;
  }

  public String getSqlText() {
    return sqlText;
  }

  public void setSqlText(String sqlText) {
    this.sqlText = sqlText;
  }

  public List<ParameterMapping> getParameterMappingList() {
    return parameterMappingList;
  }

  public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
    this.parameterMappingList = parameterMappingList;
  }
}

 

這其中用到的幾個工具類我也貼在這裡,這也是從Mybatis原始碼中拿到的,就是對sql解析處理,這裡不用過大關注。

package com.my.utils;

/**
 * @author lzh
 */
public interface TokenHandler {
  String handleToken(String content);
}

 

package com.my.utils;

import java.util.ArrayList;
import java.util.List;




public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context是引數名稱 #{id} #{username}

    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
        ParameterMapping parameterMapping = new ParameterMapping(content);
        return parameterMapping;
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public void setParameterMappings(List<ParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }

}
package com.my.utils;

/**
 * @author Clinton Begin
 */
public class GenericTokenParser {

  private final String openToken; //開始標記
  private final String closeToken; //結束標記
  private final TokenHandler handler; //標記處理器

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 該方法主要實現了配置檔案、指令碼等片段中佔位符的解析、處理工作,並返回最終需要的資料。
   * 其中,解析工作由該方法完成,處理工作是由處理器handler的handleToken()方法來實現
   */
  public String parse(String text) {
    // 驗證引數問題,如果是null,就返回空字串。
    if (text == null || text.isEmpty()) {
      return "";
    }

    // 下面繼續驗證是否包含開始標籤,如果不包含,預設不是佔位符,直接原樣返回即可,否則繼續執行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // 把text轉成字元陣列src,並且定義預設偏移量offset=0、儲存最終需要返回字串的變數builder,
    // text變數中佔位符對應的變數名expression。判斷start是否大於-1(即text中是否存在openToken),如果存在就執行下面程式碼
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判斷如果開始標記前如果有轉義字元,就不作為openToken進行處理,否則繼續處理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression變數,避免空指標或者老資料干擾。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {////存在結束標記時
          if (end > offset && src[end - 1] == '\\') {//如果結束標記前面有轉義字元時
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在轉義字元,即需要作為引數進行處理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根據引數的key(即expression)進行引數處理,返回?作為佔位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}
package com.my.utils;

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

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

解析完sql後就是建立PreparedStatement物件,並通過MappedStatement物件中記錄的引數型別,利用java反射技術進行賦值,然後執行sql,最後再通過MappedStatement物件中記錄的返回值型別對結果進行封裝,同樣是用java反射,這樣就實現了引數的動態賦值和結果的動態封裝。這就是整個Mybatis的執行流程,到這裡也就完成了IMybatis框架的編寫,下面我們進行測試。

九、將IMybatis打包到本地倉庫,在IMybatis-test中引入依賴,編寫一個使用者Pojo類、UserMapper介面和一個測試類,UserMapper.xml在上面已經提供

package com.my.pojo;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 15:57
 */
public class User {

  private Long id;

  private String name;

  public Long getId() {
    return id;
  }

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

  public String getName() {
    return name;
  }

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

  @Override
  public String toString() {
    return "User{" +
        "id=" + id +
        ", name='" + name + '\'' +
        '}';
  }
}
package com.my.dao;

import com.my.pojo.User;
import java.util.List;

public interface UserMapper {

  /**
   * 查詢所有
   * @return
   */
  List<User> findAll() ;

  /**
   * 查詢單條
   * @param user
   * @return
   */
  User findOne(User user);

  /**
   * 根據id查詢單條
   * @param id
   * @return
   */
  User findById(Long id);

  /**
   * 根據id刪除使用者
   * @param id
   * @return
   */
  int deleteById(Long id);

  /**
   * 刪除使用者
   * @param user
   * @return
   */
  int delete(User user);

  /**
   * 新增使用者
   * @param user
   * @return
   */
  int insert(User user);

  /**
   * 修改使用者
   * @param user
   * @return
   */
  int update(User user);
}
package com.my.test;

import com.my.dao.UserMapper;
import com.my.io.Resource;
import com.my.pojo.User;
import com.my.sqlSession.SqlSession;
import com.my.sqlSession.SqlSessionFactory;
import com.my.sqlSession.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

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

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:05
 */
public class IMybatisTest {

  private SqlSession sqlSession;

  @Before
  public void before() throws Exception {
    InputStream resourceAsStream = Resource.getResourceAsStream("dataSourceConfig.xml");
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
    sqlSession = sqlSessionFactory.createSqlSession();
  }

  @Test
  public void test1() {
    User user = new User();
    user.setId(2L);
    user.setName("王五");

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1 = mapper.findOne(user);
    System.out.println(user1);
  }

  @Test
  public void test2() {

    User user = new User();
    user.setId(1L);
    user.setName("王五");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<User> all = mapper.findAll();
    for (User user1 : all) {
      System.out.println(user1);
    }
  }

  @Test
  public void test3() {

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1= mapper.findById(2L);
    System.out.println(user1);
  }

  @Test
  public void test4() {

    User user = new User();
    user.setId(3L);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.delete(user);
    System.out.println(row);
  }

  @Test
  public void test5() {

    User user = new User();
    user.setId(3L);
    user.setName("王五");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.update(user);
    System.out.println(row);
  }

  @Test
  public void test6() {

    User user = new User();
    user.setId(3L);
    user.setName("張三");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.insert(user);
    System.out.println(row);
  }
}

這裡就不把全部的測試結果貼出來了,貼一個看下效果就行,可以看到控制檯正常輸出,說明我們自己寫的IMybatis沒問題,可以成功執行。

 

總結:我們可以看到最後仍然是通過JDBC完成的資料庫操作。所以到這裡我們可以知道Mybatis最終仍然是呼叫的JDBC去運算元據庫,它只不過在執行JDBC之前還多去做了這一系列解析配置檔案,封裝各個物件等等這些操作,Mybatis就是對JDBC的包裝。

 

相關文章