可怕!你沒看錯,這次確實是純手工實現一個MyBatis框架!

目錄

  • 前言

  • JDBC

  • MyBatis

  • 源碼分析

  • 前置知識

  • 原理分析

  • 自己實現一個 MyBatis 框架


前言

MyBatis是一個非常優秀的持久層應用框架,目前幾乎已經一統天下。既然是持久層框架,那麼一定是對於數據庫的操作,Java 中談到數據庫操作,一定少不了 JDBC。那麼 ,MyBatis 比傳統的 JDBC 好在哪那?MyBatis 又在哪方面做了優化那?

JDBC

如果我們需要查詢所有用戶,傳統的 JDBC 會這樣寫。


public static void main(String[] args) {
    //聲明Connection對象
    Connection con = null;
    try {
        //加載驅動程序
        Class.forName("com.mysql.jdbc.Driver");
        //創建 connection 對象
        con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password");

        //使用 connection 對象創建statement 或者 PreparedStatement 類對象,用來執行SQL語句
        Statement statement = con.createStatement();
        //要執行的SQL語句
        String sql = "select * from user";
        //3.ResultSet類,用來存放獲取的結果集!!
        ResultSet rs = statement.executeQuery(sql);

        String job = "";
        String id = "";
        while(rs.next()){
            //獲取job這列數據
            job = rs.getString("job");
            //獲取userId這列數據
            id = rs.getString("userId");

            //輸出結果
            System.out.println(id + "\t" + job);
        }
    } catch(ClassNotFoundException e) {
        e.printStackTrace();
    } catch(SQLException e) {
        //數據庫連接失敗異常處理
        e.printStackTrace();
    }catch (Exception e) {
        e.printStackTrace();
    }finally{
        rs.close();
        con.close();
    }
}

通過上面的代碼,我們可以將 JDBC 對於數據庫的操作總結爲以下幾個步驟:

  1. 加載驅動

  2. 創建連接,Connection 對象

  3. 根據 Connection 創建 Statement 或者 PreparedStatement 來執行 SQL 語句

  4. 返回結果集到 ResultSet 中

  5. 手動將 ResultSet 映射到 JavaBean 中

傳統的 JDBC 操作的問題也一目瞭然,整體非常繁瑣,也不夠靈活,執行一個 SQL 查詢就要寫一堆代碼。

# MyBatis

來看看 MyBatis 代碼如何查詢數據庫。幾行代碼就完成了數據庫查詢操作,並且將數據庫查詢出來的結果映射到了 JavaBean 中了。我們的代碼沒有加入 Spring Mybatis,加入 Spring 後整體流程會複雜很多,不方便我們理解。


//獲取 sqlSession,sqlSession 相當於傳統 JDBC 的 Conection
public static SqlSession getSqlSession(){
  InputStream configFile = new FileInputStream(filePath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
    return sqlSessionFactory.openSession();
}

//使用 sqlSession 獲得對應的 mapper,mapper 用來執行 sql 語句。
public static User get(SqlSession sqlSession, int id){
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    return userMapper.selectByPrimaryKey(id);
}

我們來對 MyBatis 操作數據庫做一個總結:

  1. 使用配置文件構建 SqlSessionFactory

  2. 使用 SqlSessionFactory 獲得 SqlSession,SqlSession 相當於傳統 JDBC 的 Conection

  3. 使用 SqlSession 得到 Mapper

  4. 用 Mapper 來執行 SQL 語句,並返回結果直接封裝到 JavaBean 中

源碼分析

大家平時應該經常使用 MyBatis 框架,對於 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我們從源碼來分析怎麼實現這些概念。

前置知識

先給出一個大部分框架的代碼流程,方便大家理解框架。下面的圖片就說明了接口、抽象類和實現類的關係,我們自己寫代碼時也要多學習這種思想。

帶着結果看過程

看源碼對於很多人來說都是一個比較枯燥和乏味的過程,如果不做抽象和總結,會覺得非常亂。另外,看源碼不要去扣某個細節,儘量從宏觀上理解它。這樣帶着結果看過程你就會知道設計者爲什麼這麼做。

先給出整個 MyBatis 框架的架構圖,大家先有一個印象:


原理分析

說明,我們講解的是原生的 MyBatis 框架,並不是與 Spring 結合的 MyBatis 框架。

還是把上面 MyBatis 操作數據庫的代碼拿過來,方便我們與源碼對照。

  //獲取 sqlSession,sqlSession 相當於傳統 JDBC 的 Conection
  public static SqlSession getSqlSession(){
     //步驟一
    InputStream configFile = new FileInputStream(filePath);
    //步驟二
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
      return sqlSessionFactory.openSession();
  }
  
  //使用 sqlSession 獲得對應的 mapper,mapper 用來執行 sql 語句。
  public static User get(SqlSession sqlSession, int id){
     //步驟三
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      return userMapper.selectByPrimaryKey(id);
  }

MyBatis 框架的第一步就是加載我們數據庫的相關信息,比如用戶名、密碼等。以及我們在 XML 文件中寫的 SQL 語句。

  //配置文件中指定了數據庫相關的信息和寫 sql 語句的 mapper 相關信息,稍後我們需要讀取並加載到我們的配置類中。
  <configuration>
    <environments default="development">
      <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/db"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
          </dataSource>
        </transactionManager>
      </environment>
    </environments>
  </configuration>
  <mappers>
    <mapper resource="xml/UserMapper.xml"/>
  </mappers>

第二步就是通過讀取到的配置文件信息,構建一個 SqlSessionFactory。

通過 openSession 方法返回了一個 sqlSession,我們來看看 openSession 方法做了什麼。

  //我們來重點看看 openSession 做了什麼操作, DefaultSqlSessionFactory.java
  @Override
  public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
  }
  
  public Configuration getConfiguration() {
    return this.configuration;
  }
  //這個函數裏面有着事務控制相關的代碼。
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
  
    DefaultSqlSession var8;
    try {
      Environment environment = this.configuration.getEnvironment();
      TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
      //根據上面的參數得到 TransactionFactory,通過 TransactionFactory 生成一個 Transaction,可以理解爲這個 SqlSession 的事務控制器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 將這個事務控制器封裝在 Executor 裏
      Executor executor = this.configuration.newExecutor(tx, execType);
      // 使用 configuration 配置類,Executor,和 configuration(是否自動提交) 來構建一個 DefaultSqlSession。
      var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
      this.closeTransaction(tx);
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
      ErrorContext.instance().reset();
    }
  
    return var8;
  }

看了上面的一大段代碼你可能會覺得蒙,沒關係,我們來劃重點,最終結果返回了一個
DefaultSqlsession。

  // 使用 configuration 配置類(我們上面讀取的配置文件就需要加載到這個類中),Executor(包含了數據事務控制相關信息),和 autoCommit(是否自動提交) 來構建一個 DefaultSqlSession。
  var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

有了這個 sqlSession 之後,我們就可以實現所有對數據庫的操作了,因爲我們已經把所有的信息加載到這裏面了。數據庫信息、SQL 信息、SQL 語句執行器等。

當然我們一般使用這個 sqlSession 獲得對應的 mapper 接口類,然後用這個接口類查詢數據庫。

既然所有東西都封裝在 sqlSession 中,先來看看 sqlSession 的組成部分。

SqlSession 的接口定義:裏面定義了增刪改查和提交回滾等方法。

  public interface SqlSession extends Closeable {
      <T> T selectOne(String var1);
  
      <T> T selectOne(String var1, Object var2);
  
      <E> List<E> selectList(String var1);
  
      <E> List<E> selectList(String var1, Object var2);
  
      <E> List<E> selectList(String var1, Object var2, RowBounds var3);
  
      <K, V> Map<K, V> selectMap(String var1, String var2);
  
      <K, V> Map<K, V> selectMap(String var1, Object var2, String var3);
  
      <K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);
  
      <T> Cursor<T> selectCursor(String var1);
  
      <T> Cursor<T> selectCursor(String var1, Object var2);
  
      <T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);
  
      void select(String var1, Object var2, ResultHandler var3);
  
      void select(String var1, ResultHandler var2);
  
      void select(String var1, Object var2, RowBounds var3, ResultHandler var4);
  
      int insert(String var1);
  
      int insert(String var1, Object var2);
  
      int update(String var1);
  
      int update(String var1, Object var2);
  
      int delete(String var1);
  
      int delete(String var1, Object var2);
  
      void commit();
  
      void commit(boolean var1);
  
      void rollback();
  
      void rollback(boolean var1);
  
      List<BatchResult> flushStatements();
  
      void close();
  
      void clearCache();
  
      Configuration getConfiguration();
  
      <T> T getMapper(Class<T> var1);
  
      Connection getConnection();
  }

接下來用 sqlSession 獲取對應的 Mapper。

DefaultSqlSession 的 getMapper 實現:


  public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
  }
  
  //從 configuration 裏面 getMapper,Mapper 就在 Configuration 裏
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry 裏 getMapper 的最終實現,同時我們需要思考一個問題,我們的 sqlSession 接口裏面只定義了抽象的增刪改查,而這個接口並沒有任何實現類,那麼這個 XML 到底是如何與接口關聯起來並生成實現類那?通過 MapperRegistry 可以得出答案,那就是動態代理。

 public class MapperRegistry {
      private final Configuration config;
      // 用一個 Map 來存儲接口和 xml 文件之間的映射關係,key 應該是接口,但是 value 是 MapperProxyFactory
      private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
  
      public MapperRegistry(Configuration config) {
          this.config = config;
      }
  
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //獲取到這個接口對應的 MapperProxyFactory。
          MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
          if (mapperProxyFactory == null) {
              throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
          } else {
              try {
                  //用上一步獲取的 MapperProxyFactory 和 sqlSession 構建對應的 Class
                  return mapperProxyFactory.newInstance(sqlSession);
              } catch (Exception var5) {
                  throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
              }
          }
      }
  }
最終的結果是生成一個 mapper 接口的動態代理

最終的結果是生成一個 mapper 接口的動態代理類,通過這個類,我們實現對數據庫的增刪改查。

接下來我們看看 newInstance 的具體實現:


  public T newInstance(SqlSession sqlSession) {
    // mapperInterface 就是接口
    MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
  }
  
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //動態代理,這裏的動態代理有一些不一樣
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
  }

爲什麼說這裏的動態代理有一些不一樣那?我們先看看正常流程的動態代理,接口,和接口實現類是必須的。而我們的 Mapper 接口只有充滿了 SQL 語句的 XML 文件,沒有具體實現類。

與傳統的動態代理相比,MyBatis 的 Mapper 接口是沒有實現類的,那麼它又是怎麼實現動態代理的那?

我們來看一下 MapperProxy 的源碼:


  public class MapperProxy<T> implements InvocationHandler, Serializable {
      private static final long serialVersionUID = -6424540398559729838L;
      private final SqlSession sqlSession;
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache;
  
      public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
          this.sqlSession = sqlSession;
          this.mapperInterface = mapperInterface;
          this.methodCache = methodCache;
      }
      // 正常的動態代理中 Object proxy 這個參數應該是接口的實現類
      // com.paul.pkg.UserMapper@5a123uf
      // 現在裏面是 org.apache.ibatis.binding.MapperProxy@6y213kn, 這倆面
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          try {
              if (Object.class.equals(method.getDeclaringClass())) {
                  return method.invoke(this, args);
              }
  
              if (this.isDefaultMethod(method)) {
                  return this.invokeDefaultMethod(proxy, method, args);
              }
          } catch (Throwable var5) {
              throw ExceptionUtil.unwrapThrowable(var5);
          }
          // Mapper 走這個流程,先嚐試在緩存裏獲取 method
          MapperMethod mapperMethod = this.cachedMapperMethod(method);
          return mapperMethod.execute(this.sqlSession, args);
      }
  
      private MapperMethod cachedMapperMethod(Method method) {
          MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
          if (mapperMethod == null) {
              // mapperMethod 的構建,通過接口名,方法,和 xml 配置(通過 sqlSession 的 Configuration 獲得)
              mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            //通過 execute 執行方法,因爲 sqlSession 封裝了 Executor,所以還要傳進來,execute 方法使用
            //sqlSession 裏面的方法。
              this.methodCache.put(method, mapperMethod);
          }
  
          return mapperMethod;
      }
  
  }

來看 MapperMethod 的定義:


  // command 裏面包含了方法名,比如 com.paul.pkg.selectByPrimaryKey
  // type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE
  // method 是方法的簽名
  public class MapperMethod {
      private final MapperMethod.SqlCommand command;
      private final MapperMethod.MethodSignature method;
  
      public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
          this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
          this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
      }
  }
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;//返回結果
   //INSERT操作
    if (SqlCommandType.INSERT == command.getType()) {
      //處理參數
      Object param = method.convertArgsToSqlCommandParam(args);
      //調用sqlSession的insert方法 
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      }
      .....
      .....
    } 

通過 sqlSession 來執行我們的 SQL 語句,返回結果,動態代理的方法調用結束。

進入 DefaultSqlSession 執行對應的 SQL 語句。


  public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }
  
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    List var5;
    try {
      // 這裏又需要 configuration 來獲取對應的 statement
      // MappedStatement 裏面有 xml 文件,和要執行的方法,就是 xml 裏面的 id,statementType,以及 sql 語句。
      MappedStatement ms = this.configuration.getMappedStatement(statement);
      // 用 executor 執行 query,executor 裏面應該是包裝了 JDBC。
      var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception var9) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
    } finally {
      ErrorContext.instance().reset();
    }
  
    return var5;
  }

Executor 的實現類裏面執行 query 方法。


public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      this.flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        this.ensureNoOutParams(ms, boundSql);
        List<E> list = (List)this.tcm.getObject(cache, key);
        if (list == null) {
          list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          this.tcm.putObject(cache, key, list);
        }
  
        return list;
      }
    }
    // 使用 delegate 去 query,delegate 是 SimpleExecutor。裏面使用 JDBC 進行數據庫操作。
    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

# 自己實現一個 MyBatis 框架

整體流程

image

  1. 首先創建 SqlSessionFactory 實例,SqlSessionFactory 就是創建 SqlSession 的工廠類。

  2. 加載配置文件創建 Configuration 對象,配置文件包括數據庫相關配置文件以及我們在 XML 文件中寫的 SQL。

  3. 通過 SqlSessionFactory 創建 SqlSession。

  4. 通過 SqlSession 獲取 mapper 接口動態代理。

  5. 動態代理回調 SqlSession 中某查詢方法。

  6. SqlSession 將查詢方法轉發給 Executor。

  7. Executor 基於 JDBC 訪問數據庫獲取數據,最後還是通過 JDBC 操作數據庫。

  8. Executor 通過反射將數據轉換成 POJO 並返回給 SqlSession。

  9. 將數據返回給調用者。

項目整體使用 Maven 構建,mybatis-demo 是脫離 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代碼就不在講解了。paul-mybatis 是我們自己實現的 MyBatis 框架。關注公衆號Java面試那些事兒,回覆關鍵字面試,獲取最新的面試資料。

首先按照我們以前的使用 MyBatis 代碼時的流程,創建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,這幾個文件我們和 mybatis-demo 創建一樣的即可,方便我們比較結果。

Mapper 接口,這裏面定義兩個抽象方法,根據主鍵查找用戶和查找所有用戶:


  package com.paul.mybatis.mapper;
  import com.paul.mybatis.entity.User;
  import java.util.List;
  
  public interface UserMapper {
  
      User selectByPrimaryKey(long userId);
      List<User> selectAll();
  }

XML 文件,裏面是上面兩個抽象方法的具體 SQL 實現,完全消防官方 XML 文件的寫法,需要注意 namespace、id、resultType、SQL 語句這幾個點,都是我們後面代碼需要處理的。


  <?xml version="1.0" encoding="UTF-8" ?>
  <!DOCTYPE mapper
          PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
          "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  <mapper namespace="com.paul.mybatis.mapper.UserMapper">
  
      <select id="selectByPrimaryKey" resultType="User">
          select *
          from t_user
          where userId = #{userId}
      </select>
  
      <select id="selectAll" resultType="User">
          select *
          from t_user
      </select>
  
  </mapper>

最後,是我們的實體類,它的屬性與數據庫的表相對應:

  package com.paul.mybatis.entity;
  
  public class User {
  
      private long userId;
      private String userName;
      private int sex;
      private String role;
  
      public long getUserId() {
          return userId;
      }
      public void setUserId(long userId) {
          this.userId = userId;
      }
      public String getUserName() {
          return userName;
      }
      public void setUserName(String userName) {
          this.userName = userName;
      }
      public int getSex() {
          return sex;
      }
      public void setSex(int sex) {
          this.sex = sex;
      }
      public String getRole() {
          return role;
      }
      public void setRole(String role) {
          this.role = role;
      }
  }

最後一個配置文件,數據庫連接配置文件 db.propreties:


  jdbc.driver=com.mysql.jdbc.Driver
  jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
  jdbc.username=root
  jdbc.password=root

配置文件和一些測試的必須類已經寫完了,首先我們需要把這些配置信息加載到 Configuration 配置類中。關注公衆號Java面試那些事兒,回覆關鍵字面試,獲取最新的面試資料。

先定義一個類來加載寫 SQL 語句的 XML 文件,上面我們說過要注意四個點,namespace、id、resultType、SQL 語句,我們寫對應的屬性來保存它,代碼很簡單,就不多講了。

  package com.paul.mybatis.confiuration;
  
  
  /**
   *
   * XML 中的 sql 配置信息加載到這個類中
   *
   */
  public class MappedStatement {
  
      private String namespace;
  
      private String id;
  
      private String resultType;
  
      private String sql;
  
      public String getNamespace() {
          return namespace;
      }
  
      public void setNamespace(String namespace) {
          this.namespace = namespace;
      }
  
      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 getSql() {
          return sql;
      }
  
      public void setSql(String sql) {
          this.sql = sql;
      }
  }

接下來我們定義一個 Configuration 總配置類,來保存 db.propeties 裏面的屬性和 XML 文件的 SQL 信息,Configuration 類裏面的文件對應我們配置文件中的屬性。


  package com.paul.mybatis.confiuration;
  
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   *
   * 所有的配置信息
   *
   */
  public class Configuration {
  
      private String jdbcDriver;
  
      private String jdbcUrl;
  
      private String jdbcPassword;
  
      private String jdbcUsername;
  
      private Map<String,MappedStatement> mappedStatement = new HashMap<>();
  
      public Map<String, MappedStatement> getMappedStatement() {
          return mappedStatement;
      }
  
      public void setMappedStatement(Map<String, MappedStatement> mappedStatement) {
          this.mappedStatement = mappedStatement;
      }
  
      public String getJdbcDriver() {
          return jdbcDriver;
      }
  
      public void setJdbcDriver(String jdbcDriver) {
          this.jdbcDriver = jdbcDriver;
      }
  
      public String getJdbcUrl() {
          return jdbcUrl;
      }
  
      public void setJdbcUrl(String jdbcUrl) {
          this.jdbcUrl = jdbcUrl;
      }
  
      public String getJdbcPassword() {
          return jdbcPassword;
      }
  
      public void setJdbcPassword(String jdbcPassword) {
          this.jdbcPassword = jdbcPassword;
      }
  
      public String getJdbcUsername() {
          return jdbcUsername;
      }
  
      public void setJdbcUsername(String jdbcUsername) {
          this.jdbcUsername = jdbcUsername;
      }
  }

按照上面的流程圖,我們來創建一個 SqlSessionFactory 工廠類,這個類有兩個功能,一個是加載配置文件信息到 Configuration 類中,另一個是創建 SqlSession。

SqlSessionFactory 抽象模版:


  package com.paul.mybatis.factory;
  
  import com.paul.mybatis.sqlsession.SqlSession;
  
  public interface SqlSessionFactory {
      SqlSession openSession();
  }

創建 SqlSessionFactory 的 Default 實現類,Default 實現類主要完成了兩個功能,加載配置信息到 Configuration 對象裏,實現創建 SqlSession 的功能。

 
  package com.paul.mybatis.factory;
  
  import com.paul.mybatis.confiuration.Configuration;
  import com.paul.mybatis.confiuration.MappedStatement;
  import com.paul.mybatis.sqlsession.DefaultSqlSession;
  import com.paul.mybatis.sqlsession.SqlSession;
  import org.dom4j.Document;
  import org.dom4j.DocumentException;
  import org.dom4j.Element;
  import org.dom4j.io.SAXReader;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Properties;
  
  /**
   *
   * 1.初始化時就完成了 configuration 的實例化
   * 2.工廠類,生成 sqlSession
   *
   */
  public class DefaultSqlSessionFactory implements SqlSessionFactory{
  
      private final Configuration configuration = new Configuration();
  
      // xml 文件存放的位置
      private static final String MAPPER_CONFIG_LOCATION = "mappers";
  
      // 數據庫信息存放的位置
      private static final String DB_CONFIG_FILE = "db.properties";
  
  
      public DefaultSqlSessionFactory() {
          loadDBInfo();
          loadMapperInfo();
      }
  
      private void loadDBInfo() {
          InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
          Properties p = new Properties();
  
          try {
              p.load(db);
          } catch (IOException e) {
              e.printStackTrace();
          }
          //將配置信息寫入Configuration 對象
          configuration.setJdbcDriver(p.get("jdbc.driver").toString());
          configuration.setJdbcUrl(p.get("jdbc.url").toString());
          configuration.setJdbcUsername(p.get("jdbc.username").toString());
          configuration.setJdbcPassword(p.get("jdbc.password").toString());
  
      }
  
      //解析並加載xml文件
      private void loadMapperInfo(){
          URL resources = null;
          resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
          File mappers = new File(resources.getFile());
          //讀取文件夾下面的文件信息
          if(mappers.isDirectory()){
              File[] files = mappers.listFiles();
              for(File file:files){
                  loadMapperInfo(file);
              }
          }
      }
  
      private void loadMapperInfo(File file){
          SAXReader reader = new SAXReader();
          //通過read方法讀取一個文件轉換成Document 對象
          Document document = null;
          try {
              document = reader.read(file);
          } catch (DocumentException e) {
              e.printStackTrace();
          }
          //獲取根結點元素對象<mapper>
          Element e = document.getRootElement();
          //獲取命名空間namespace
          String namespace = e.attribute("namespace").getData().toString();
          //獲取select,insert,update,delete子節點列表
          List<Element> selects = e.elements("select");
          List<Element> inserts = e.elements("insert");
          List<Element> updates = e.elements("update");
          List<Element> deletes = e.elements("delete");
  
          List<Element> all = new ArrayList<>();
          all.addAll(selects);
          all.addAll(inserts);
          all.addAll(updates);
          all.addAll(deletes);
  
          //遍歷節點,組裝成 MappedStatement 然後放入到configuration 對象中
          for(Element ele:all){
              MappedStatement mappedStatement = new MappedStatement();
              String id = ele.attribute("id").getData().toString();
              String resultType = ele.attribute("resultType").getData().toString();
              String sql = ele.getData().toString();
  
              mappedStatement.setId(namespace+"."+id);
              mappedStatement.setResultType(resultType);
              mappedStatement.setNamespace(namespace);
              mappedStatement.setSql(sql);
        // xml 文件中的每個 sql 方法都組裝成 mappedStatement 對象,以 namespace+"."+id 爲 key, 放入
        // configuration 配置類中。
              configuration.getMappedStatement().put(namespace+"."+id,mappedStatement);
          }
      }
  
      @Override
      public SqlSession openSession() {
        // openSession 方法創建一個 DefaultSqlSession,configuration 配置類作爲 構造函數參數傳入
          return new DefaultSqlSession(configuration);
      }
  }

在 SqlSessionFactory 裏創建了 DefaultSqlSession,我們看看它的具體實現。SqlSession 裏面應該封裝了所有數據庫的具體操作和一些獲取 mapper 實現類的方法。

SqlSession 接口,定義模版方法
  package com.paul.mybatis.sqlsession;
  
  import java.util.List;
  
  /**
   *
   * 封裝了所有數據庫的操作
   * 所有功能都是基於 Excutor 來實現的,Executor 封裝了 JDBC 操作
   *
   *
   */
  public interface SqlSession {
      /**
       * 根據傳入的條件查詢單一結果
       * @param statement  namespace+id,可以用做 key,去 configuration 裏面獲取 sql 語句,resultType
       * @param parameter  要傳入 sql 語句中的查詢參數
       * @param <T> 返回指定的結果對象
       * @return
       */
      <T> T selectOne(String statement, Object parameter);
      <T> List<T> selectList(String statement, Object parameter);
      <T> T getMapper(Class<T> type);
  }  

Default 的 SqlSession 實現類。裏面需要傳入 Executor,這個 Executor 裏面封裝了 JDBC 操作數據庫的流程。我們重點關注 getMapper 方法,使用動態代理生成一個加強類。關注公衆號Java面試那些事兒,回覆關鍵字面試,獲取最新的面試資料。這裏面最終還是把數據庫的相關操作轉給 SqlSession,使用 Mapper 能使編程更加優雅。

  package com.paul.mybatis.sqlsession;
  
  import com.paul.mybatis.bind.MapperProxy;
  import com.paul.mybatis.confiuration.Configuration;
  import com.paul.mybatis.confiuration.MappedStatement;
  import com.paul.mybatis.executor.Executor;
  import com.paul.mybatis.executor.SimpleExecutor;
  
  import java.lang.reflect.Proxy;
  import java.util.List;
  
  public class DefaultSqlSession implements  SqlSession {
  
      private final Configuration configuration;
  
      private Executor executor;
  
      public DefaultSqlSession(Configuration configuration) {
          super();
          this.configuration = configuration;
          executor = new SimpleExecutor(configuration);
      }
  
      @Override
      public <T> T selectOne(String statement, Object parameter) {
          List<T> selectList = this.selectList(statement,parameter);
          if(selectList == null || selectList.size() == 0){
              return null;
          }
          if(selectList.size() == 1){
              return (T) selectList.get(0);
          }else{
              throw new RuntimeException("too many result");
          }
      }
  
      @Override
      public <T> List<T> selectList(String statement, Object parameter) {
          MappedStatement ms = configuration.getMappedStatement().get(statement);
          // 我們的查詢方法最終還是交給了 Executor 去執行,Executor 裏面封裝了 JDBC 操作。傳入參數包含了 sql 語句和 sql 語句需要的參數。
          return executor.query(ms,parameter);
      }
  
      @Override
      public <T> T getMapper(Class<T> type) {
        //通過動態代理生成了一個實現類,我們重點關注,動態代理的實現,它是一個 InvocationHandler,傳入參數是 this,就是 sqlSession 的一個實例。
          MapperProxy mp = new MapperProxy(this);
          //給我一個接口,還你一個實現類
          return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);
      }
  }

來看看我們的 InvocationHandler 如何實現 invoke 方法:

  package com.paul.mybatis.bind;

  import com.paul.mybatis.sqlsession.SqlSession;
  
  import java.lang.reflect.InvocationHandler;
  import java.lang.reflect.Method;
  import java.util.Collection;
  import java.util.Collections;
  
  /**
   *
   * 將請求轉發給 sqlSession
   *
   */
  public class MapperProxy implements InvocationHandler {
  
      private SqlSession sqlSession;
  
      public MapperProxy(SqlSession sqlSession) {
          this.sqlSession = sqlSession;
      }
  
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          System.out.println(method.getDeclaringClass().getName()+"."+method.getName());
          //最終還是將執行方法轉給 sqlSession,因爲 sqlSession 裏面封裝了 Executor
          //根據調用方法的類名和方法名以及參數,傳給 sqlSession 對應的方法
          if(Collection.class.isAssignableFrom(method.getReturnType())){
              return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);
          }else{
              return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);
          }
      }
  }

獲取 Mapper 接口的實現類我們已經實現了,通過動態代理調用 sqlSession 的方法。那麼就剩最後一個重要的工作了,那就是實現 Exectuor 類去操作數據庫,封裝 JDBC。

Executor 抽象模版,我們只實現了 query、update 等操作慢慢增加。

  package com.paul.mybatis.executor;
  
  import com.paul.mybatis.confiuration.MappedStatement;
  
  import java.util.List;

  /**
   *
   * mybatis 核心接口之一,定義了數據庫操作的最基本的方法,JDBC,sqlSession的所有功能都是基於它來實現的
   *
   */
  public interface Executor {
  
      /**
       *
       * 查詢接口
       * @param ms 封裝sql 語句的 mappedStatemnet 對象,裏面包含了 sql 語句,resultType 等。
       * @param parameter 傳入sql 參數
       * @param <E> 將數據對象轉換成指定對象結果集返回
       * @return
       */
      <E> List<E> query(MappedStatement ms, Object parameter);
  
  }

Executor 接口的實現類,主要是對 JDBC 的封裝,和利用反射方法將結果映射到 resultType 對應的實體類中

java
package com.paul.mybatis.executor;


import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.util.ReflectionUtil;


import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class SimpleExecutor implements Executor {

    private final Configuration configuration;

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

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter) {
        System.out.println(ms.getSql().toString());

        List<E> ret = new ArrayList<>(); //返回結果集
        try {
            Class.forName(configuration.getJdbcDriver());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());
         String regex = "#\\{([^}])*\\}";
          // 將 sql 語句中的 #{userId} 替換爲 ?
          String  sql = ms.getSql().replaceAll(regex,"");
          preparedStatement = connection.prepareStatement(sql);
            //處理佔位符,把佔位符用傳入的參數替換
            parametersize(preparedStatement, parameter);
            resultSet = preparedStatement.executeQuery();
            handlerResultSet(resultSet, ret,ms.getResultType());
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                resultSet.close();
                preparedStatement.close();
                connection.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        return ret;
    }


    private void parametersize(PreparedStatement preparedStatement,Object parameter) throws SQLException{
        if(parameter instanceof Integer){
            preparedStatement.setInt(1,(int)parameter);
        }else if(parameter instanceof  Long){
            preparedStatement.setLong(1,(Long)parameter);
        }else if(parameter instanceof  String){
            preparedStatement.setString(1,(String)parameter);
        }
    }

    private <E> void handlerResultSet(ResultSet resultSet, List<E> ret,String className){
        Class<E> clazz = null;
        //通過反射獲取類對象
        try {
            clazz = (Class<E>)Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


        try {
            while (resultSet.next()) {
                Object entity = clazz.newInstance();
                //通過反射工具 將 resultset 中的數據填充到 entity 中
                ReflectionUtil.setPropToBeanFromResultSet(entity, resultSet);
                ret.add((E) entity);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

看一下測試的結果,整個 MyBatis 框架已經實現完成了,當然有很多地方需要完善,比如 XML 中的 SQL 語句處處理還缺很多功能,目前只支持 select 等,希望大家能通過源碼解讀和自己寫的過程明白 MyBatis 的具體實現要點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章