從Mybatis源碼分析其工作原理

Mybatis工作原理也是面試的一大考點,必須要對其非常清晰,這樣才能懟回去。本文建立在Spring+SpringMVC+Mybatis整合的項目之上。

我將其工作原理分爲六個部分:

  1. 讀取核心配置文件並返回InputStream流對象。
  2. 根據InputStream流對象解析出Configuration對象,然後創建SqlSessionFactory工廠對象
  3. 根據一系列屬性從SqlSessionFactory工廠中創建SqlSession
  4. SqlSession中調用Executor執行數據庫操作&&生成具體SQL指令
  5. 對執行結果進行二次封裝
  6. 提交與事務

先給大家看看我的實體類:

/**
 * 圖書實體
 */
public class Book {

    private long bookId;// 圖書ID

    private String name;// 圖書名稱

    private int number;// 館藏數量

        getter and setter ...
}

1. 讀取核心配置文件

1.1 配置文件mybatis-config.xml

<?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>
    <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://xxx.xxx:3306/ssm" />
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
        <mappers>
        <mapper resource="BookMapper.xml"/>
    </mappers>
</configuration>

當然,還有很多可以在XML 文件中進行配置,上面的示例指出的則是最關鍵的部分。要注意 XML 頭部的聲明,用來驗證 XML 文檔正確性。environment 元素體中包含了事務管理和連接池的配置。mappers 元素則是包含一組 mapper 映射器(這些 mapper 的 XML 文件包含了 SQL 代碼和映射定義信息)。

1.2 BookMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="Book">
    <!-- 目的:爲dao接口方法提供sql語句配置 -->
    <insert id="insert" >
        insert into book (name,number) values (#{name},#{number})
    </insert>
</mapper>

就是一個普通的mapper.xml文件。

1.3 Main方法

從 XML 文件中構建 SqlSessionFactory 的實例非常簡單,建議使用類路徑下的資源文件進行配置。但是也可以使用任意的輸入流(InputStream)實例,包括字符串形式的文件路徑或者 file:// 的 URL 形式的文件路徑來配置。MyBatis 包含一個名叫 Resources 的工具類,它包含一些實用方法,可使從 classpath 或其他位置加載資源文件更加容易。

public class Main {
    public static void main(String[] args) throws IOException {
        // 創建一個book對象
        Book book = new Book();
        book.setBookId(1006);
        book.setName("Easy Coding");
        book.setNumber(110);
        // 加載配置文件 並構建SqlSessionFactory對象
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
        // 從SqlSessionFactory對象中獲取 SqlSession對象
        SqlSession sqlSession = factory.openSession();
        // 執行操作
        sqlSession.insert("insert", book);
        // 提交操作
        sqlSession.commit();
        // 關閉SqlSession
        sqlSession.close();
    }
}

這個代碼是根據Mybatis官方提供的一個不使用 XML 構建 SqlSessionFactory的一個Demo改編的。

注意:是官方給的一個不使用 XML 構建 SqlSessionFactory的例子,那麼我們就從這個例子中查找入口來分析。

2. 根據配置文件生成SqlSessionFactory工廠對象

2.1 Resources.getResourceAsStream(resource);源碼分析

Resources是mybatis提供的一個加載資源文件的工具類。

我們只看getResourceAsStream方法:

public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream((ClassLoader)null, resource);
}

getResourceAsStream調用下面的方法:

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    } else {
        return in;
    }
}

獲取到自身的ClassLoader對象,然後交給ClassLoader(lang包下的)來加載:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    ClassLoader[] arr$ = classLoader;
    int len$ = classLoader.length;

    for(int i$ = 0; i$ < len$; ++i$) {
        ClassLoader cl = arr$[i$];
        if (null != cl) {
            InputStream returnValue = cl.getResourceAsStream(resource);
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }

            if (null != returnValue) {
                return returnValue;
            }
        }
    }

值的注意的是,它返回了一個InputStream對象。

2.2 new SqlSessionFactoryBuilder().build(inputStream);源碼分析

public SqlSessionFactoryBuilder() {
}

所以new SqlSessionFactoryBuilder()只是創建一個對象實例,而沒有對象返回(建造者模式),對象的返回交給build()方法。

public SqlSessionFactory build(InputStream inputStream) {
    return this.build((InputStream)inputStream, (String)null, (Properties)null);
}

這裏要傳入一個inputStream對象,就是將我們上一步獲取到的InputStream對象傳入。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        // 進行XML配置文件的解析
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();

        try {
            inputStream.close();
        } catch (IOException var13) {
            ;
        }

    }

    return var5;
}

如何解析的就大概說下,通過Document對象來解析,然後返回InputStream對象,然後交給XMLConfigBuilder構造成org.apache.ibatis.session.Configuration對象,然後交給build()方法構造程SqlSessionFactory:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
}

3. 創建SqlSession

SqlSession 完全包含了面向數據庫執行 SQL 命令所需的所有方法。你可以通過 SqlSession 實例來直接執行已映射的 SQL 語句。

public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}

調用自身的openSessionFromDataSource方法:

  1. getDefaultExecutorType()默認是SIMPLE。
  2. 注意TX等級是 Null, autoCommit是false。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        // 根據Configuration的Environment屬性來創建事務工廠
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        // 從事務工廠中創建事務,默認等級爲null,autoCommit=false
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 創建執行器
        Executor executor = this.configuration.newExecutor(tx, execType);
        // 根據執行器創建返回對象 SqlSession
        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;
}

構建步驟:
Environment>>TransactionFactory+autoCommit+tx-level>>Transaction+ExecType>>Executor+Configuration+autoCommit>>SqlSession

其中,EnvironmentConfiguration中的屬性。

4. 調用Executor執行數據庫操作&&生成具體SQL指令

在拿到SqlSession對象後,我們調用它的insert方法。

public int insert(String statement, Object parameter) {
    return this.update(statement, parameter);
}

它調用了自身的update(statement, parameter)方法:

public int update(String statement, Object parameter) {
    int var4;
    try {
        this.dirty = true;
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        // wrapCollection(parameter)判斷 param對象是否是集合
        var4 = this.executor.update(ms, this.wrapCollection(parameter));
    } catch (Exception var8) {
        throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
    } finally {
        ErrorContext.instance().reset();
    }

    return var4;
}

mappedStatements就是我們平時說的sql映射對象.

源碼如下:
protected final Map<String, MappedStatement> mappedStatements;

可見它是一個Map集合,在我們加載xml配置的時候,mapping.xmlnamespaceid信息就會存放爲mappedStatementskey,對應的,sql語句就是對應的value.

然後調用BaseExecutor中的update方法:

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        this.clearLocalCache();
        // 真正做執行操作的方法
        return this.doUpdate(ms, parameter);
    }
}

doUpdate纔是真正做執行操作的方法:

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;

    int var6;
    try {
        Configuration configuration = ms.getConfiguration();
        // 創建StatementHandler對象,從而創建Statement對象
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
        // 將sql語句和參數綁定並生成SQL指令
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        var6 = handler.update(stmt);
    } finally {
        this.closeStatement(stmt);
    }

    return var6;
}

先來看看prepareStatement方法,看看mybatis是如何將sql拼接合成的:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Connection connection = this.getConnection(statementLog);
    // 準備Statement
    Statement stmt = handler.prepare(connection);
    // 設置SQL查詢中的參數值
    handler.parameterize(stmt);
    return stmt;
}

來看看parameterize方法:

public void parameterize(Statement statement) throws SQLException {
    this.parameterHandler.setParameters((PreparedStatement)statement);
}

這裏把statement轉換程PreparedStatement對象,它比Statement更快更安全。
這都是我們在JDBC中熟用的對象,就不做介紹了,所以也能看出來Mybatis是對JDBC的封裝。

從ParameterMapping中讀取參數值和類型,然後設置到SQL語句中:

public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
    if (parameterMappings != null) {
        for(int i = 0; i < parameterMappings.size(); ++i) {
            ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                String propertyName = parameterMapping.getProperty();
                Object value;
                if (this.boundSql.hasAdditionalParameter(propertyName)) {
                    value = this.boundSql.getAdditionalParameter(propertyName);
                } else if (this.parameterObject == null) {
                    value = null;
                } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
                    value = this.parameterObject;
                } else {
                    MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
                    value = metaObject.getValue(propertyName);
                }

                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = this.configuration.getJdbcTypeForNull();
                }

                try {
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException var10) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);
                } catch (SQLException var11) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var11, var11);
                }
            }
        }
    }

}

5. 對查詢結果二次封裝

在doUpdate方法中,解析生成完新的SQL後,然後執行var6 = handler.update(stmt);我們來看看它的源碼。

public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement)statement;
     // 執行sql
    ps.execute();
    // 獲取返回值
    int rows = ps.getUpdateCount();
    Object parameterObject = this.boundSql.getParameterObject();
    KeyGenerator keyGenerator = this.mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(this.executor, this.mappedStatement, ps, parameterObject);
    return rows;
}

因爲我們是插入操作,返回的是一個int類型的值,所以這裏mybatis給我們直接返回int。

如果是query操作,返回的是一個ResultSet,mybatis將查詢結果包裝程ResultSetWrapper類型,然後一步步對應java類型賦值等...有興趣的可以自己去看看。

6. 提交與事務

最後,來看看commit()方法的源碼。

public void commit() {
    this.commit(false);
}

調用其對象本身的commit()方法:

public void commit(boolean force) {
    try {
        // 是否提交(判斷是提交還是回滾)
        this.executor.commit(this.isCommitOrRollbackRequired(force));
        this.dirty = false;
    } catch (Exception var6) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
    } finally {
        ErrorContext.instance().reset();
    }
}

如果dirty是false,則進行回滾;如果是true,則正常提交。

private boolean isCommitOrRollbackRequired(boolean force) {
    return !this.autoCommit && this.dirty || force;
}

調用CachingExecutor的commit方法:

public void commit(boolean required) throws SQLException {
    this.delegate.commit(required);
    this.tcm.commit();
}

調用BaseExecutor的commit方法:

public void commit(boolean required) throws SQLException {
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
            this.transaction.commit();
        }

    }
}

最後調用JDBCTransaction的commit方法:

public void commit() throws SQLException {
    if (this.connection != null && !this.connection.getAutoCommit()) {
        if (log.isDebugEnabled()) {
            log.debug("Committing JDBC Connection [" + this.connection + "]");
        }
        // 提交連接
        this.connection.commit();
    }
}

Demo參考文檔:http://www.mybatis.org/mybatis-3/zh/getting-started.html

GitHub地址: https://github.com/fantj2016/java-reader

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