你分析過mybatis工作原理嗎?
Mybatis工作原理也是面試的一大考點,必須要對其非常清晰,這樣才能懟回去。本文建立在Spring+SpringMVC+Mybatis整合的專案之上。
感謝原文出處:https://www.cnblogs.com/javazhiyin/
我將其工作原理分為六個部分:
-
讀取核心配置檔案並返回
InputStream
流物件。 -
根據
InputStream
流物件解析出Configuration
物件,然後建立SqlSessionFactory
工廠物件 -
根據一系列屬性從
SqlSessionFactory
工廠中建立SqlSession
-
從
SqlSession
中呼叫Executor
-
對執行結果進行二次封裝
-
提交與事務
先給大家看看我的實體類:
/** * 圖書實體 */ 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
方法:
-
getDefaultExecutorType()預設是SIMPLE。
-
注意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
其中,Environment
是Configuration
中的屬性。
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.xml
的namespace
和id
資訊就會存放為mappedStatements
的key
,對應的,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(); } }