MyBatis詳解(一)

憂愁的chafry 發表於 2022-11-27
MyBatis

MyBatis簡單介紹

【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,物件關係對映】,使用簡單,學習成本較低。可以執行自己手寫的SQL語句,比較靈活。但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自己修改配置,所以稱只為半自動ORM框架。

 

傳統JDBC介紹

【1】簡單使用

@Test
public  void test() throws SQLException {
    Connection conn=null;
    PreparedStatement pstmt=null;
    try {
        // 1.載入驅動,其實這一步可以不加因為DriverManager裡面會有自動載入驅動的一步
        Class.forName("com.mysql.jdbc.Driver");

        // 2.建立連線
        conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");

        //開啟事務
        conn.setAutoCommit(false);

        // SQL語句
        String sql="select id,user_name,create_time from t_user where id=? ";

        // 獲得sql執行者
        pstmt=conn.prepareStatement(sql);
        pstmt.setInt(1,1);

        // 執行查詢
        //ResultSet rs= pstmt.executeQuery();
        pstmt.execute();
        ResultSet rs= pstmt.getResultSet();

        rs.next();
        User user =new User();
        user.setId(rs.getLong("id"));
        user.setUserName(rs.getString("user_name"));
        user.setCreateTime(rs.getDate("create_time"));
        System.out.println(user.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally{
        // 關閉資源
        try {
            if(conn!=null){
                conn.close();
            }
            if(pstmt!=null){
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

【1.1】DriverManager如何自動載入驅動【利用靜態程式碼塊呼叫初始化方法,至於為什麼要使用SPI機制,主要是為了實現解耦和可插拔,因為驅動有多種】

  注:驅動展示【位於mysql-connector-java-5.1.22.jar/META-INF/services/java.sql.Driver  內容為:com.mysql.jdbc.Driver】

static {
    // 啟動類載入器載入DriverManager類,觸發靜態方法執行
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    // 載入java.sql.Driver驅動的實現
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 1、建立一個 ServiceLoader物件,【這裡就將上下文類載入器設定到ServiceLoader物件的變數上了】
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 2、建立一個迭代器物件
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                // 3、這裡呼叫driversIterator.hasNext()的時候,觸發將 META-INF/services 下的配置檔案中的資料讀取進來,方便下面的next方法使用
                while(driversIterator.hasNext()) {
                    // 4、【關鍵】:觸發上面建立的迭代器物件的方法呼叫。這裡才是具體載入的實現邏輯,非常不好找
                    driversIterator.next();
                }
            } catch(Throwable t) {}
            return null;
        }
    });

    //判斷有沒有載入到
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {...}
    }
}

//ServiceLoader類#hasNext方法
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 1、拼湊要讀取的檔案的全名
            // final String PREFIX = "META-INF/services/";
            String fullName = PREFIX + service.getName();

            // 2、根據 fullName 去到META-INF/services/目錄下尋找配置檔案
            // 如果類載入器為空,則使用系統類載入器,如果不為空則使用指定的類載入器
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }

        // 3、使用parse方法解析配置檔案中的每一行資料
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

 

【2】當然正常情況一般是封裝成類來使用的,如

//資料訪問基類
public class BaseDao {
    // 驅動
    private static String DRIVER = null;
    // 連結字串
    private static String URL = null;
    // 使用者名稱
    private static String USERNAME = null;
    // 密碼
    private static String PASSWORD = null;

    //初始化
    static {  init();  }

    // 初始化
    private static void init() {
        try {
            // 使用Properties物件讀取資原始檔屬性
            Properties pro = new Properties();
            // 獲得資原始檔輸入流
            InputStream inStream = BaseDao.class.getClassLoader().getResourceAsStream("db.properties");
            // 載入輸入流
            pro.load(inStream);
            DRIVER = pro.getProperty("mysql.driverClass");
            URL = pro.getProperty("mysql.jdbcUrl");
            USERNAME = pro.getProperty("mysql.user");
            PASSWORD = pro.getProperty("mysql.password");

            Class.forName(DRIVER);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //獲取資料庫連線物件
    protected Connection getConnection() {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return conn;
    }

    /**
     * 關閉所有連結
     * @param conn
     * @param stmt
     * @param rs
     */
    protected void CloseAll(Connection conn, Statement stmt, ResultSet rs) {
        try {
            if (conn != null) {
                conn.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * 執行 增、刪、 改的公共方法
     * @param sql     SQL語句
     * @param prams   引數
     * @return           受影響的行數
     */
    protected int executeUpdate(String sql, Object... prams) {
        // 獲得資料庫連結物件
        Connection conn = getConnection();
        // 宣告SQL執行者
        PreparedStatement pstmt = null;
        try {
            // 獲得SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入引數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }
            // 執行executeUpdate 返回受影響行數
            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 關閉所有需要關閉的物件
            CloseAll(conn, pstmt, null);
        }

        return 0;
    }

    /**
     * 執行查詢 返回單個值
     * @param sql         SQL語句
     * @param prams     引數
     * @return             OBJECT
     */
    protected Object executeScaler(String sql, Object... prams) {
        // 獲得資料庫連結物件
        Connection conn = getConnection();
        // 宣告SQL執行者
        PreparedStatement pstmt = null;
        // 宣告查詢結果集
        ResultSet rs = null;
        // 接收單個值
        Object value = null;
        try {
            // 獲得SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入引數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }
            // 執行executeUpdate 返回受影響行數
            rs = pstmt.executeQuery();

            if (rs.next()) {
                value = rs.getObject(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            CloseAll(conn, pstmt, rs);
        }
        return value;
    }

    /**
     * 執行查詢返回list
     * 
     * @param sql         SQL語句
     * @param clazz     類的型別
     * @return List
     */
    public <T> List<T> executeList(String sql, Class<T> clazz, Object... prams) {
        // 資料集合
        List<T> list = new ArrayList<T>();
        // 獲得資料庫連線
        Connection conn = getConnection();
        // 宣告SQL執行者
        PreparedStatement pstmt = null;
        // 宣告查詢結果集
        ResultSet rs = null;
        try {

            // 3. 透過連結建立一個SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入引數
            for (int i = 0; i < prams.length; i++) {
                //內部會透過instance of判斷這個引數到底是哪個型別的具體物件
                pstmt.setObject(i + 1, prams[i]);
            }

            // 4 執行查詢SQL 返回查詢結果
            rs = pstmt.executeQuery();

            // 獲得結果集的列資訊物件
            ResultSetMetaData rsmd = rs.getMetaData();

            while (rs.next()) {
                // 透過類反射例項化
                T obj = clazz.newInstance();

                // 迴圈所有的列
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    /* 透過屬性名稱使用反射給泛型例項賦值 Begin */

                    // 獲得每一列的列名
                    String cloName = rsmd.getColumnName(i);
                    // 根據列名反射到類的欄位
                    Field filed = clazz.getDeclaredField(cloName);
                    // 設定私有屬性的訪問許可權
                    filed.setAccessible(true);

                    // 給泛型例項的某一個屬性賦值
                    filed.set(obj, rs.getObject(cloName));
                    /* 透過屬性名稱使用反射給泛型例項賦值 End */
                }
                // 將泛型例項新增到 泛型集合中
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return list;
    }

    /**
     * 執行查詢返回JavaBean
     * 
     * @param sql         SQL語句
     * @param clazz     類的型別
     * @return JavaBean
     */
    public <T> T executeJavaBean(String sql, Class<T> clazz, Object... prams) {
        // 宣告資料物件
        T obj=null;
        // 獲得資料庫連線
        Connection conn = getConnection();
        // 宣告SQL執行者
        PreparedStatement pstmt = null;
        // 宣告查詢結果集
        ResultSet rs = null;
        try {

            // 3. 透過連結建立一個SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入引數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }

            // 4 執行查詢SQL 返回查詢結果
            rs = pstmt.executeQuery();

            // 獲得結果集的列資訊物件
            ResultSetMetaData rsmd = rs.getMetaData();

            if (rs.next()) {
                // 透過類反射例項化
                obj = clazz.newInstance();

                // 迴圈所有的列
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    /* 透過屬性名稱使用反射給泛型例項賦值 Begin */

                    // 獲得每一列的列名
                    String cloName = rsmd.getColumnName(i);
                    // 根據列名反射到類的欄位
                    Field filed = clazz.getDeclaredField(cloName);
                    // 設定私有屬性的訪問許可權
                    filed.setAccessible(true);

                    // 給泛型例項的某一個屬性賦值
                    filed.set(obj, rs.getObject(cloName));
                    /* 透過屬性名稱使用反射給泛型例項賦值 End */
                } 
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return obj;
    }
}

 

【3】總結JDBC的四大核心物件

1DriverManager(驅動管理物件):獲取資料庫連線;
2Connection(資料庫連線物件):獲取執行sql物件;管理事務;
3Statement(執行sql物件):executeUpdate執行DML語句(增刪改)DDL語句;executeQuery 執行DQL語句;
4ResultSet(結果集物件)

 

【4】傳統JDBC的問題

1)資料庫連線建立,釋放頻繁造成西戎資源的浪費,從而影響系統效能,使用資料庫連線池可以解決問題。
2)sql語句在程式碼中硬編碼,造成程式碼的不已維護,實際應用中sql的變化可能較大,sql程式碼和java程式碼沒有分離開來維護不方便。
3)使用preparedStatement向有佔位符傳遞引數存在硬編碼問題因為sql中的where子句的條件不確定,同樣是修改不方便。
4)對結果集中解析存在硬編碼問題,sql的變化導致解析程式碼的變化,系統維護不方便。

【5】針對問題的最佳化

1、資料庫連線建立、釋放頻繁造成系統資源浪費從而影響系統效能,如果使用資料庫連線池可解決此問題。
最佳化部分,如mybatis:在SqlMapConfig.xml中配置資料連線池,使用連線池管理資料庫連結。

2、Sql語句寫在程式碼中造成程式碼不易維護,實際應用sql變化的可能較大,sql變動需要改變java程式碼。
最佳化部分,如mybatis:將Sql語句配置在XXXXmapper.xml檔案中與java程式碼分離。

3、向sql語句傳引數麻煩,因為sql語句的where條件不一定,可能多也可能少,佔位符需要和引數一一對應。
最佳化部分,如mybatis:自動將java物件對映至sql語句,透過statement中的parameterType定義輸入引數的型別。

4、對結果集解析麻煩,sql變化導致解析程式碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成pojo物件解析比較方便。
最佳化部分,如mybatis:自動將sql執行結果對映至java物件,透過statement中的resultType定義輸出結果的型別。

 

Mybaits整體體系圖

【1】圖示

          MyBatis詳解(一)

【2】分析

把Mybatis的功能架構分為三層:
1基礎支撐層:負責最基礎的功能支撐,包括連線管理、事務管理、配置載入和快取處理,這些都是共用的東西,將他們抽取出來作為最基礎的元件。為上層的資料處理層提供最基礎的支撐。
對應示例中的部分為:  SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

2API介面層:提供給外部使用的介面API,開發人員透過這些本地API來操縱資料庫。介面層一接收到呼叫請求就會呼叫資料處理層來完成具體的資料處理。
對應示例中的部分為:  SqlSession session = sqlMapper.openSession();

3資料處理層:負責具體的SQL查詢、SQL解析、SQL執行和執行結果對映處理等。它主要的目的是根據呼叫的請求完成一次資料庫操作。
對應示例中的部分為:  
User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);
或者
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);

 

【3】簡單示例

public static void main(String[] args) {
    String resource = "mybatis-config.xml";
    Reader reader;
    try {
        //將XML配置檔案構建為Configuration配置類
        reader = Resources.getResourceAsReader(resource);
        // 透過載入配置檔案流構建一個SqlSessionFactory  DefaultSqlSessionFactory
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        // 資料來源 執行器  DefaultSqlSession
        SqlSession session = sqlMapper.openSession();
        try {
            // 執行查詢 底層執行jdbc
            //User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);

            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.selectById(1L);
            
            session.commit();
            System.out.println(user.getUserName());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            session.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

Mybaits外掛的分析

【1】外掛主要作用於四⼤元件物件

1)執⾏器 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等⽅法)
2)引數處理器 ParameterHandler (getParameterObject, setParameters 等⽅法)
3)結果集處理器 ResultSetHandler (handleResultSets, handleOutputParameters 等⽅法)
4)SQL語法構建處理器 StatementHandler (prepare, parameterize, batch, update, query 等⽅法)    

 

原始碼分析部分

MyBatis解析全域性配置檔案的原始碼分析

【1】分析怎麼透過載入配置檔案流構建一個SqlSessionFactory

public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
 }

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
}

// 到這裡配置檔案已經解析成了Configuration
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

 

【1.1】分析XMLConfigBuilder類怎麼將xml的資原始檔流解析成Configuration

【1.1.1】新建XMLConfigBuilder過程【所以說XMLConfigBuilder並不負責解析,解析的是它裡面的XPathParser類,】

/**
* 建立一個用於解析xml配置的構建器物件
* @param inputStream 傳入進來的xml的配置
* @param environment 我們的環境變數
* @param props:用於儲存我們從xml中解析出來的屬性
*/
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    /**
     * 該方法做了二個事情
     * 第一件事情:建立XPathParser 解析器物件,在這裡會把我們的
     * 把我們的mybatis-config.xml解析出一個Document物件
     * 第二節事情:呼叫重寫的建構函式來構建我XMLConfigBuilder
     */
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    //呼叫父類的BaseBuilder的構造方法:給configuration賦值,typeAliasRegistry別名註冊器賦值,TypeHandlerRegistry賦值
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");

    //把props繫結到configuration的props屬性上
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
}

//又因為class XMLConfigBuilder extends BaseBuilder,所以看一下他的父類
public abstract class BaseBuilder {
    // mybatis的全域性配置檔案
    protected final Configuration configuration;
    // 用於儲存我們的Entity的別名
    protected final TypeAliasRegistry typeAliasRegistry;
    // 使用者儲存我們java型別和jdbc資料庫型別的
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }
    ....
}

 

【1.1.2】parser.parse()解析過程【本質上就是從根結點開始解析

public Configuration parse() {
    //若已經解析過了 就丟擲異常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    //設定解析標誌位,保證只解析一次
    parsed = true;
    /**
     * 解析我們的mybatis-config.xml的節點
     * <configuration> </configuration>
     */
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

//方法實現說明:解析我們mybatis-config.xml的 configuration節點
private void parseConfiguration(XNode root) {
    try {
      // 解析 properties節點
      // 如:<properties resource="mybatis/db.properties" />
      propertiesElement(root.evalNode("properties"));
      
      // 解析我們的mybatis-config.xml中的settings節點
      // 如:<setting name="mapUnderscoreToCamelCase" value="false"/>
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      
      // 基本沒有用過該屬性
      // VFS含義是虛擬檔案系統;主要是透過程式能夠方便讀取本地檔案系統、FTP檔案系統等系統中的檔案資源。
      // Mybatis中提供了VFS這個配置,主要是透過該配置可以載入自定義的虛擬檔案系統應用程式
      loadCustomVfs(settings);
      
      // 指定 MyBatis 所用日誌的具體實現,未指定時將自動查詢。
      // SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
      // 解析到org.apache.ibatis.session.Configuration#logImpl
      loadCustomLogImpl(settings);

      // 解析別名typeAliases節點
      typeAliasesElement(root.evalNode("typeAliases"));

      // 解析外掛節點(比如分頁外掛),解析到 interceptorChain.interceptors
      pluginElement(root.evalNode("plugins"));

      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 設定settings 到 configuration 裡面
      settingsElement(settings);

      // 解析mybatis環境,解析到:org.apache.ibatis.session.Configuration#environment
      // 在整合spring情況下由 spring-mybatis提供資料來源 和事務工廠
      environmentsElement(root.evalNode("environments"));
      
      // 解析資料庫廠商
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      
      // 解析型別處理器節點
      typeHandlerElement(root.evalNode("typeHandlers"));

      // 解析mapper節點(這個是最重要的)
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException(...);
    }
}

 

【1.1.2.1】展示xml配置檔案與Mapper檔案

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- properties 掃描屬性檔案.properties  -->
    <properties resource="db.properties"></properties>

    <!-- 具體可以配置哪些屬性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings  -->
    <settings>
        <!-- 把資料庫欄位對映為駝峰式的欄位 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 內部的屬性可以配置日誌,常用的日誌是:LOG4J 、 STDOUT_LOGGING,用於列印執行的sql語句長什麼樣 -->
        <setting name="logImpl" value="LOG4J"/>
<!--        <setting name="cacheEnabled" value="true"/>-->
<!--        <setting name="lazyLoadingEnabled" value="true"/>-->
<!--        <setting name="localCacheScope" value="SESSION"/>-->
<!--        <setting name="jdbcTypeForNull" value="OTHER"/>-->
    </settings>

    <!-- 別名設定,常有單個設定【如com.project.entity.User,就是類的全路徑】,或者按路徑設定,本質上就是將其設定於 Configuration 或者 BaseBuilder中,可以用於查詢返回結果的對映   -->
<!--    <typeAliases>-->
<!--        <typeAlias alias="User" type="com.project.entity.User"/>-->
<!--    </typeAliases>-->
<!--    <typeAliases>-->
<!--        <package name="com.project.entity"/>-->
<!--    </typeAliases>-->

    <!-- 外掛設定 -->
   <!-- <plugins>
        <plugin interceptor="com.project.plugins.ExamplePlugin" ></plugin>
    </plugins>-->

    <!-- 型別處理器設定 -->
    <typeHandlers>
        <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
    </typeHandlers>

    <environments default="development">
        <environment id="development">
           <transactionManager type="JDBC"/>
            <!--//  mybatis內建了JNDI、POOLED、UNPOOLED三種型別的資料來源,其中POOLED對應的實現為org.apache.ibatis.datasource.pooled.PooledDataSource,它是mybatis自帶實現的一個同步、執行緒安全的資料庫連線池 一般在生產中,我們會使用c3p0或者druid連線池-->
            <dataSource type="POOLED">
                <property name="driver" value="${mysql.driverClass}"/>
                <property name="url" value="${mysql.jdbcUrl}"/>
                <property name="username" value="${mysql.user}"/>
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!-- resource:註冊class類路徑下的-->
        <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
        <mapper class="com.project.mapper.DeptMapper"></mapper>

        <package name="com.mapper"/>
    </mappers>
</configuration>

 

【1.1.2.2】部分內容分析

【1.1.2.2.1】解析 properties節點

// 本質上這個分離方式便是為了解耦
// 大體上看來載入properties有兩種,一種是本地檔案,一種是遠端檔案
// 而且載入出來的資料其實是會放置於configuration屬性裡面的,也就是XMLConfigBuilder類裡面
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
}

//下面便展示一下db.properties的常用內容
mysql.driverClass=com.mysql.jdbc.Driver
mysql.jdbcUrl=jdbc:mysql://localhost:3306/mybatis_example?characterEncoding=utf8
mysql.user= root
mysql.password= 123456

 

【1.1.2.2.2】解析我們的mybatis-config.xml中的settings節點

private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    //分析配置的節點
    Properties props = context.getChildrenAsProperties();

    // 其實就是去configuration類裡面拿到所有setter方法,看看有沒有當前的配置項
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
}

 

【1.1.2.2.3】解析別名typeAliases節點

// 別名設定,別名設定最常用的就是對於entity【也就是實體類物件】
// 常常作用域Mapper的resultMap屬性,也就是查詢結果的對映,對映到對於的實體類上
// 由下面分析可以看出來是具備單個或者對包類路徑的掃描
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            Class<?> clazz = Resources.classForName(type);
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
}

 

【1.1.2.2.4】解析外掛節點(比較重要)

// 對於外掛的解析,很明顯的有用到裝飾器模式的概念,其實都是封裝成了interceptor
// 然後統一存放於configuration裡面
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

//實際上是封裝成了這個類,因為Interceptor是介面,是不具備例項化能力的
@Intercepts({})
public class ExamplePlugin implements Interceptor {
  private Properties properties;

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }

  public Properties getProperties() {
    return properties;
  }

}

 

【1.1.2.2.5】解析mybatis環境

//這裡面主要的就是事務工廠和資料來源的設定
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
              TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
              DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
              DataSource dataSource = dsFactory.getDataSource();
              Environment.Builder environmentBuilder = new Environment.Builder(id)
                  .transactionFactory(txFactory)
                  .dataSource(dataSource);
              configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

//展示一下一般可以在xml裡面怎麼配置
<environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="root"/>
            <property name="password" value="Zw726515"/>
        </dataSource>
    </environment>

    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>

//其實裡面的JDBC和POOLED是怎麼來的呢,都是源於別名那部分的,在Configuration類建構函式里面,他就會為別名裡面註冊一部分常用資料
public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
}

 

【1.1.2.2.6】型別處理器解析

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 根據包類路徑註冊
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                typeHandlerRegistry.register(typeHandlerPackage);
            } else {
                //根據單個進行註冊,屬性有 handler(處理器) ,jdbcType(對應資料庫型別) ,javaType(對應java型別)
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                if (javaTypeClass != null) {
                if (jdbcType == null) {
                      typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                } else {
                      typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                }
                } else {
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

 

【1.1.2.2.7】最重要的mapper解析,明顯是有四種型別

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // 獲取我們mappers節點下的一個一個的mapper節點
        for (XNode child : parent.getChildren()) {
            // 判斷我們mapper是不是透過批次註冊的,如<package name="com.project.mapper"></package>
            if ("package".equals(child.getName())) {
                  String mapperPackage = child.getStringAttribute("name");
                  configuration.addMappers(mapperPackage);
            } else {
                  // 判斷從classpath下讀取我們的mapper,如<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
                  String resource = child.getStringAttribute("resource");

                  // 判斷是不是從我們的網路資源讀取(或者本地磁碟得),如<mapper url="D:/mapper/EmployeeMapper.xml"/>
                  String url = child.getStringAttribute("url");

                  // 解析這種型別(要求介面和xml在同一個包下),如<mapper class="com.project.mapper.DeptMapper"></mapper>
                  String mapperClass = child.getStringAttribute("class");

                 // 拿<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>這種進行分析
                  if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);

                    // 把我們的檔案讀取出一個流
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    // 建立讀取XmlMapper構建器物件,用於來解析我們的mapper.xml檔案
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());

                    // 真正的解析我們的mapper.xml配置檔案(說白了就是來解析我們的sql)
                    mapperParser.parse();

                  } else if (resource == null && url != null && mapperClass == null) {

                      // 這種從網路資源讀取,與上面的大體一致就不解釋了
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();

                  } else if (resource == null && url == null && mapperClass != null) {

                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                    
                  } else {
                      // 都不是則丟擲異常
                    throw new BuilderException(...);
                  }
            }
        }
    }
}

 

【1.1.2.2.7.1】分析addMapper方法最後是怎麼解析的

// MapperRegistry類的兩個屬性值
// private final Configuration config;
// private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

//Configuration類#addMapper方法
// 傳入包路徑的方法
public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
}

//說白了也就是從包路徑下面拿到所有的類,然後呼叫傳入類的方法
public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
        // 呼叫的也是核心方法1
          addMapper(mapperClass);
    }
}

// 傳入類的方法
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

// 核心方法1
// 方法實現說明:把我們的Mapper class儲存到我們的knownMappers map 中
public <T> void addMapper(Class<T> type) {
    // 判斷我們傳入進來的type型別是不是介面
    if (type.isInterface()) {
      // 判斷我們的快取中有沒有該型別
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          boolean loadCompleted = false;
          try {
            // 建立一個MapperProxyFactory 把我們的Mapper介面儲存到工廠類中
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // mapper註解構造器
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);

            // 進行解析
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
    }
}

 

【1.1.2.2.7.2】分析parse()方法怎麼解析,其實有兩種方式,一種是 XMLMapperBuilder類#parse方法,另一種是 MapperAnnotationBuilder類#parse方法

【1.1.2.2.7.2.1】分析XMLMapperBuilder類#parse方法

// XMLMapperBuilder類#parse方法
public void parse() {
    // 判斷當前的Mapper是否被載入過
    if (!configuration.isResourceLoaded(resource)) {
      // 真正的解析我們的 <mapper namespace="com.project.mapper.EmployeeMapper">
      configurationElement(parser.evalNode("/mapper"));
      // 把資源儲存到我們Configuration中
      configuration.addLoadedResource(resource);

      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}


// 方法實現說明:解析我們的<mapper></mapper>節點
private void configurationElement(XNode context) {
    try {
        // 解析我們的namespace屬性 <mapper namespace="com.project.mapper.EmployeeMapper">
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        // 儲存我們當前的namespace  並且判斷介面完全類名==namespace
        builderAssistant.setCurrentNamespace(namespace);

        // 解析快取引用
        // 示例說明:說明當前的快取引用和DeptMapper的快取引用一致,<cache-ref namespace="com.project.mapper.DeptMapper"></cache-ref>
        // 解析到org.apache.ibatis.session.Configuration#cacheRefMap<當前namespace,ref-namespace>
        // 異常下(引用快取未使用快取):org.apache.ibatis.session.Configuration#incompleteCacheRefs
        cacheRefElement(context.evalNode("cache-ref"));

        // 解析cache節點,如<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
        // 解析到:org.apache.ibatis.session.Configuration#caches
        // org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
        cacheElement(context.evalNode("cache"));

        // 解析paramterMap節點(該節點mybaits3.5貌似不推薦使用了)
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));

        // 解析resultMap節點
        // 解析到:org.apache.ibatis.session.Configuration#resultMaps
        // 異常 org.apache.ibatis.session.Configuration#incompleteResultMaps
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        
        // 解析sql節點
        // 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments,其實等於 org.apache.ibatis.session.Configuration#sqlFragments,因為他們是同一引用,在構建XMLMapperBuilder 時把Configuration.getSqlFragments傳進去了
        sqlElement(context.evalNodes("/mapper/sql"));

        // 解析我們的select | insert |update |delete節點,解析到org.apache.ibatis.session.Configuration#mappedStatements
        // 實際上便是將每個語句都構建成mappedStatement物件
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
          throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

 

【1.1.2.2.7.2.1.1】MappedStatement類結構展示

public final class MappedStatement {
    private String resource;//mapper配置檔名,如:UserMapper.xml
    private Configuration configuration;//全域性配置
    private String id;//節點的id屬性加名稱空間,如:com.lucky.mybatis.dao.UserMapper.selectByExample
    private Integer fetchSize;
    private Integer timeout;//超時時間
    private StatementType statementType;//操作SQL的物件的型別
    private ResultSetType resultSetType;//結果型別
    private SqlSource sqlSource;//sql語句
    private Cache cache;//快取
    private ParameterMap parameterMap;
    private List<ResultMap> resultMaps;
    private boolean flushCacheRequired;
    private boolean useCache;//是否使用快取,預設為true
    private boolean resultOrdered;//結果是否排序
    private SqlCommandType sqlCommandType;//sql語句的型別,如select、update、delete、insert
    private KeyGenerator keyGenerator;
    private String[] keyProperties;
    private String[] keyColumns;
    private boolean hasNestedResultMaps;
    private String databaseId;//資料庫ID
    private Log statementLog;
    private LanguageDriver lang;
    private String[] resultSets;
}

 

【1.1.2.2.7.2.1.2】解析cache節點部分cacheElement方法解析

//這裡面便是二級快取的產生
private void cacheElement(XNode context) {
    if (context != null) {
        //解析cache節點的type屬性
        String type = context.getStringAttribute("type", "PERPETUAL");

        //根據type的String獲取class型別
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);

        // 獲取快取過期策略:預設是LRU,
        // LRU - 最近最少使用:移除最長時間不被使用的物件(預設)
        // FIFO - 先進先出:按物件進入快取的順序來移除他們
        // SOFT - 軟引用:基於垃圾回收器狀態和軟引用規則移除物件
        // WEAK - 弱引用:更積極的基於垃圾收集器狀態和弱引用規則移除物件
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);

        //flushInterval(重新整理間隔)屬性可以被設定為任意的正整數,設定的值應該是一個以毫秒為單位的合理時間量。 預設情況是不設定,也就是沒有重新整理間隔,快取僅僅會在呼叫語句時重新整理。
        Long flushInterval = context.getLongAttribute("flushInterval");
        //size(引用數目)屬性可以被設定為任意正整數,要注意欲快取物件的大小和執行環境中可用的記憶體資源。預設值是 1024。
        Integer size = context.getIntAttribute("size");
        //只讀)屬性可以被設定為 true 或 false。只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此這些物件不能被修改。這就提供了可觀的效能提升。而可讀寫的快取會(透過序列化)返回快取物件的複製。 速度上會慢一些,但是更安全,因此預設值是 false
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        //把快取節點加入到Configuration中
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

【1.1.2.2.7.2.1.2.0】分析快取是如何封裝的

public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

public Cache build() {
    setDefaultImplementations();
    //這裡便是設定了PerpetualCache為最基礎層的cache
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    //在這裡利用decorators作為包裝器進行包裝
    if (PerpetualCache.class.equals(cache.getClass())) {
      //便會存在LruCache【 delegate屬性-》PerpetualCache】,利用了反射
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      //這裡又會包裝別的
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
}

private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);//ScheduledCache:排程快取,負責定時清空快取
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {  // 將LRU 裝飾到Serialized
            cache = new SerializedCache(cache); //SerializedCache:快取序列化和反序列化儲存
        }
        cache = new LoggingCache(cache);  //日誌記錄層包裝
        cache = new SynchronizedCache(cache);  //執行緒同步層包裝
        if (blocking) { //判斷是否有進行防穿透設定
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

 

1.1.2.2.7.2.1.2.1對cache的分析

//Cache本身只是一個介面,用於定義快取
public interface Cache {
    String getId();

    void putObject(Object key, Object value);

    Object getObject(Object key);

    Object removeObject(Object key);

    void clear();

    int getSize();

    default ReadWriteLock getReadWriteLock() { return null; }
}

//重點在於它的實現類
//四種具備淘汰機制的cache
LruCache:(最近最少使用)防溢位快取區
FifoCache:(先進先出)
WeakCache:基於弱引用實現的快取管理策略
SoftCache:基於軟引用實現的快取管理策略


//底層的cache
PerpetualCache:真正儲存快取的地方

//額外輔助功能的cache
ScheduledCache:過期清理快取區
SynchronizedCache:執行緒同步快取區
LoggingCache:統計命中率以及列印日誌
SerializedCache:快取序列化和反序列化儲存
BlockingCache:防穿透

1.1.2.2.7.2.1.2.1.1對cache的最常用的兩種淘汰策略分析

【1.1.2.2.7.2.1.2.1.1.1】對LRU分析:【這個本質上就是利用LinkedHashMap】

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      //當put進新值方法但會true時,便移除該map中最老的鍵和值
      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}

 

【1.1.2.2.7.2.1.2.1.1.2】對FIFO分析:【這個本質上就是利用LinkedList】

public class FifoCache implements Cache {

    private final Cache delegate;
    private final Deque<Object> keyList;
    private int size;

    public FifoCache(Cache delegate) {
        this.delegate = delegate;
        this.keyList = new LinkedList<>();
        this.size = 1024;
    }

    @Override
    public String getId() {
        return delegate.getId();
    }

    @Override
    public int getSize() {
        return delegate.getSize();
    }

    public void setSize(int size) {
        this.size = size;
    }

    //可以看出都是移除頭部節點,將新的塞入尾結點
    @Override
    public void putObject(Object key, Object value) {
        cycleKeyList(key);
        delegate.putObject(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return delegate.getObject(key);
    }

    @Override
    public Object removeObject(Object key) {
        return delegate.removeObject(key);
    }

    @Override
    public void clear() {
        delegate.clear();
        keyList.clear();
    }

    //加到最後,移除最前
    private void cycleKeyList(Object key) {
        keyList.addLast(key);
        if (keyList.size() > size) {
            Object oldestKey = keyList.removeFirst();
            delegate.removeObject(oldestKey);
        }
    }

}

 

1.1.2.2.7.2.1.3解析sql節點部分sqlElement方法解析

private void buildStatementFromContext(List<XNode> list) {
    // 判斷有沒有配置資料庫廠商ID
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

/**
* 方法實現說明:解析select|update|delte|insert節點然後建立mapperStatment物件
* @param list:所有的select|update|delte|insert節點
* @param requiredDatabaseId:判斷有沒有資料庫廠商Id
*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    // 迴圈select|delte|insert|update節點
    for (XNode context : list) {
      // 建立一個xmlStatement的構建器物件
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
}

 

1.1.2.2.7.2.1.3.1】深入分析是怎麼解析的:

public void parseStatementNode() {
    // insert|delte|update|select 語句的sqlId
    String id = context.getStringAttribute("id");
    // 判斷insert|delte|update|select  節點是否配置了資料庫廠商標註
    String databaseId = context.getStringAttribute("databaseId");

    // 匹配當前的資料庫廠商id是否匹配當前資料來源的廠商id
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    // 獲得節點名稱:select|insert|update|delete
    String nodeName = context.getNode().getNodeName();
    // 根據nodeName 獲得 SqlCommandType列舉
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 判斷是不是select語句節點
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //  獲取flushCache屬性,預設值為isSelect的反值:查詢:預設flushCache=false,增刪改:預設flushCache=true
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // 獲取useCache屬性,預設值為isSelect:查詢:預設useCache=true,增刪改:預設useCache=false
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);

    // resultOrdered:  是否需要處理巢狀查詢結果 group by (使用極少),可以將比如 30條資料的三組資料  組成一個巢狀的查詢結果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 解析sql公用片段,如
    //<select id="qryEmployeeById" resultType="Employee" parameterType="int">
    //     <include refid="selectInfo"></include>
    //    employee where id=#{id}
    //</select>
    //將 <include refid="selectInfo"></include> 解析成sql語句 放在<select>Node的子節點中
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析我們sql節點的引數型別
    String parameterType = context.getStringAttribute("parameterType");
    // 把引數型別字串轉化為class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    // 檢視sql是否支撐自定義語言,如
    // <delete id="delEmployeeById" parameterType="int" lang="tulingLang">
    // <settings>
    //         setting name="defaultScriptingLanguage" value="tulingLang"/>
    //    </settings>
    String lang = context.getStringAttribute("lang");
    // 獲取自定義sql指令碼語言驅動 預設:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // 解析<insert 語句的的selectKey節點, 一般在oracle裡面設定自增id
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // insert語句 用於主鍵生成元件
    KeyGenerator keyGenerator;
    /**
     * selectById!selectKey
     * id+!selectKey
     */
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    //把名稱空間拼接到keyStatementId中 , com.tuling.mapper.Employee.saveEmployee!selectKey
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    /**
     *<insert id="saveEmployee" parameterType="com.tuling.entity.Employee" useGeneratedKeys="true" keyProperty="id">
     *判斷全域性的配置類configuration中是否包含以及解析過的元件生成器物件
     */
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {

      /**
       * 若配置了useGeneratedKeys 那麼就去除useGeneratedKeys的配置值,
       * 否者就看mybatis-config.xml配置檔案中是配置了
       * <setting name="useGeneratedKeys" value="true"></setting> 預設是false
       * 並且判斷sql操作型別是否為insert
       * 若是的話,那麼使用的生成策略就是Jdbc3KeyGenerator.INSTANCE
       * 否則就是NoKeyGenerator.INSTANCE
       */
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 透過class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver來解析我們的sql指令碼物件.  
    // 解析SqlNode. 注意:只是解析成一個個的SqlNode,並不會完全解析sql,因為這個時候引數都沒確定,動態sql無法解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

    // STATEMENT,PREPARED 或 CALLABLE 中的一個。
    // 這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

    // 這是一個給驅動的提示,嘗試讓驅動程式每次批次返回的結果行數和這個設定值相等。 預設值為未設定(unset)(依賴驅動)
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // 這個設定是在丟擲異常之前,驅動程式等待資料庫返回請求結果的秒數。預設值為未設定(unset)(依賴驅動)。
    Integer timeout = context.getIntAttribute("timeout");
    // 將會傳入這條語句的引數類的完全限定名或別名。這個屬性是可選的,因為 MyBatis 可以透過型別處理器(TypeHandler) 推斷出具體傳入語句的引數,預設值為未設定
    String parameterMap = context.getStringAttribute("parameterMap");
    // 從這條語句中返回的期望型別的類的完全限定名或別名。 注意如果返回的是集合,那應該設定為集合包含的型別,而不是集合本身。
    // 可以使用 resultType 或 resultMap,但不能同時使用
    String resultType = context.getStringAttribute("resultType");
    // 解析我們查詢結果集返回的型別 
    Class<?> resultTypeClass = resolveClass(resultType);
    // 外部 resultMap 的命名引用。結果集的對映是 MyBatis 最強大的特性,如果你對其理解透徹,許多複雜對映的情形都能迎刃而解。
    // 可以使用 resultMap 或 resultType,但不能同時使用。
    String resultMap = context.getStringAttribute("resultMap");

    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }

    // 解析 keyProperty  keyColumn 僅適用於 insert 和 update
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    // 為我們的insert|delete|update|select節點構建成我們的mappedStatment物件
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

 

【1.1.2.2.7.2.1.3.1.1】深入分析SqlNode的生成

@Override
//方法實現說明:建立sqlSource物件
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      // 動態Sql源(需要引數才能確定的sql語句)
      // 如:select id,user_name,create_time from t_user where id=${param1} ,這種是拼接的
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 靜態Sql源(不需要透過引數就能確定的sql語句),它會在這裡解析
      // 如:select id,user_name,create_time from t_user where id=#{param1} ,因為是會把 #{param1} 這部分用?替換
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

// 解析${} 和 動態節點
/**
 * 遞迴解析  selectById這個sql元素會解析成
 *    1層  MixedSqlNode <SELECT>
 *    2層  WhereSqlNode <WHERE>
 *    2層  IfSqlNode <IF>
 *       test="條件表示式"
 *
 *  contexts= sql語句分: 1.TextSqlNode 帶${}    2.StaticTextSqlNode
 */
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();  //獲得<select>的子節點
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody(""); // 獲得sql文字
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {  // 怎樣算Dynamic? 其實就是判斷sql文字中有${}
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();

        // 判斷當前節點是不是動態sql節點{@link XMLScriptBuilder#initNodeHandlerMap()}
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);  // 不同動態節點有不用的實現
        isDynamic = true;     // 怎樣算Dynamic? 其實就是判斷sql文字動態sql節點
      }
    }
    return new MixedSqlNode(contents);
}

// SqlNode的型別
// ChooseSqlNode
// ForEachSqlNode
// IfSqlNode
// StaticTextSqlNode
// TextSqlNode
// MixedSqlNode:如果某階段還包含其他SqlNode節點,用這個進行包裝
// SetSqlNode
// WhereSqlNode
// TrimSqlNode
//如示例:
<select id="selectById"  resultMap="result" >
    select id,user_name,create_time from t_user 
    <where>
        <if test="id>0">
           and id=#{id}
        </if>
    </where>
</select>

//劃分情況:
MixedSqlNode
    StaticTextSqlNode
    WhereSqlNode
        MixedSqlNode
            IfSqlNode
                MixedSqlNode
                    StaticTextSqlNode

 

【1.1.2.2.7.2.2】分析MapperAnnotationBuilder#parse方法

// MapperAnnotationBuilder類#parse方法
public void parse() {
    String resource = type.toString();
    // 是否已經解析mapper介面對應的xml
    if (!configuration.isResourceLoaded(resource)) {
        // 根據mapper介面名獲取 xml檔案並解析,解析<mapper></mapper>裡面所有東西放到configuration
        loadXmlResource();
        // 新增已解析的標記
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        // 獲取所有方法 看是不是用了註解
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            try {
              if (!method.isBridge()) {
                // 是不是用了註解  用了註解會將註解解析成MappedStatement
                parseStatement(method);
              }
            } catch (IncompleteElementException e) {
                  configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

 

引數

相關文章