Mybatis工作原理也是面試的一大考點,必須要對其非常清晰,這樣才能懟回去。本文建立在Spring+SpringMVC+Mybatis整合的項目之上。
我將其工作原理分為六個部分:
-
讀取核心配置文件並返回
InputStream流對象。 -
根據
InputStream流對象解析出Configuration對象,然后創建SqlSessionFactory工廠對象 -
根據一系列屬性從
SqlSessionFactory工廠中創建SqlSession -
從
SqlSession中調用Executor執行數據庫操作&&生成具體SQL指令 -
對執行結果進行二次封裝
-
提交與事務
先給大家看看我的實體類:
1 /** 2 * 圖書實體 3 */ 4 public class Book { 5 6 private long bookId;// 圖書ID 7 8 private String name;// 圖書名稱 9 10 private int number;// 館藏數量 11 12 getter and setter ... 13 }
1. 讀取核心配置文件
1.1 配置文件mybatis-config.xml
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 <configuration> 6 <environments default="development"> 7 <environment id="development"> 8 <transactionManager type="JDBC"/> 9 <dataSource type="POOLED"> 10 <property name="driver" value="com.mysql.jdbc.Driver"/> 11 <property name="url" value="jdbc:mysql://xxx.xxx:3306/ssm" /> 12 <property name="username" value="root"/> 13 <property name="password" value="root"/> 14 </dataSource> 15 </environment> 16 </environments> 17 <mappers> 18 <mapper resource="BookMapper.xml"/> 19 </mappers> 20 </configuration>
當然,還有很多可以在XML 文件中進行配置,上面的示例指出的則是最關鍵的部分。要注意 XML 頭部的聲明,用來驗證 XML 文檔正確性。environment 元素體中包含了事務管理和連接池的配置。mappers 元素則是包含一組 mapper 映射器(這些 mapper 的 XML 文件包含了 SQL 代碼和映射定義信息)。
1.2 BookMapper.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 5 <mapper namespace="Book"> 6 <!-- 目的:為dao接口方法提供sql語句配置 --> 7 <insert id="insert" > 8 insert into book (name,number) values (#{name},#{number}) 9 </insert> 10 </mapper>
就是一個普通的mapper.xml文件。
1.3 Main方法
從 XML 文件中構建 SqlSessionFactory 的實例非常簡單,建議使用類路徑下的資源文件進行配置。但是也可以使用任意的輸入流(InputStream)實例,包括字符串形式的文件路徑或者 file:// 的 URL 形式的文件路徑來配置。
MyBatis 包含一個名叫 Resources 的工具類,它包含一些實用方法,可使從 classpath 或其他位置加載資源文件更加容易。
1 public class Main { 2 public static void main(String[] args) throws IOException { 3 // 創建一個book對象 4 Book book = new Book(); 5 book.setBookId(1006); 6 book.setName("Easy Coding"); 7 book.setNumber(110); 8 // 加載配置文件 並構建SqlSessionFactory對象 9 String resource = "mybatis-config.xml"; 10 InputStream inputStream = Resources.getResourceAsStream(resource); 11 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); 12 // 從SqlSessionFactory對象中獲取 SqlSession對象 13 SqlSession sqlSession = factory.openSession(); 14 // 執行操作 15 sqlSession.insert("insert", book); 16 // 提交操作 17 sqlSession.commit(); 18 // 關閉SqlSession 19 sqlSession.close(); 20 } 21 }
這個代碼是根據Mybatis官方提供的一個不使用 XML 構建 SqlSessionFactory的一個Demo改編的。
注意:是官方給的一個
不使用 XML 構建 SqlSessionFactory的例子,那么我們就從這個例子中查找入口來分析。
2. 根據配置文件生成SqlSessionFactory工廠對象
2.1 Resources.getResourceAsStream(resource);源碼分析
Resources是mybatis提供的一個加載資源文件的工具類。

我們只看getResourceAsStream方法:
1 public static InputStream getResourceAsStream(String resource) throws IOException { 2 return getResourceAsStream((ClassLoader)null, resource); 3 }
getResourceAsStream調用下面的方法:
1 public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException { 2 InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader); 3 if (in == null) { 4 throw new IOException("Could not find resource " + resource); 5 } else { 6 return in; 7 } 8 }
獲取到自身的ClassLoader對象,然后交給ClassLoader(lang包下的)來加載:
1 InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { 2 ClassLoader[] arr$ = classLoader; 3 int len$ = classLoader.length; 4 5 for(int i$ = 0; i$ < len$; ++i$) { 6 ClassLoader cl = arr$[i$]; 7 if (null != cl) { 8 InputStream returnValue = cl.getResourceAsStream(resource); 9 if (null == returnValue) { 10 returnValue = cl.getResourceAsStream("/" + resource); 11 } 12 13 if (null != returnValue) { 14 return returnValue; 15 } 16 } 17 }
值的注意的是,它返回了一個InputStream對象。
2.2 new SqlSessionFactoryBuilder().build(inputStream);源碼分析
1 public SqlSessionFactoryBuilder() { 2 }
所以new SqlSessionFactoryBuilder()只是創建一個對象實例,而沒有對象返回(建造者模式),對象的返回交給build()方法。
1 public SqlSessionFactory build(InputStream inputStream) { 2 return this.build((InputStream)inputStream, (String)null, (Properties)null); 3 }
這里要傳入一個inputStream對象,就是將我們上一步獲取到的InputStream對象傳入。
1 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { 2 SqlSessionFactory var5; 3 try { 4 // 進行XML配置文件的解析 5 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); 6 var5 = this.build(parser.parse()); 7 } catch (Exception var14) { 8 throw ExceptionFactory.wrapException("Error building SqlSession.", var14); 9 } finally { 10 ErrorContext.instance().reset(); 11 12 try { 13 inputStream.close(); 14 } catch (IOException var13) { 15 ; 16 } 17 18 } 19 20 return var5; 21 }
如何解析的就大概說下,通過Document對象來解析,然后返回InputStream對象,然后交給XMLConfigBuilder構造成org.apache.ibatis.session.Configuration對象,然后交給build()方法構造程SqlSessionFactory:
1 public SqlSessionFactory build(Configuration config) { 2 return new DefaultSqlSessionFactory(config); 3 } 4 public DefaultSqlSessionFactory(Configuration configuration) { 5 this.configuration = configuration; 6 }
3. 創建SqlSession
SqlSession 完全包含了面向數據庫執行 SQL 命令所需的所有方法。你可以通過 SqlSession 實例來直接執行已映射的 SQL 語句。
1 public SqlSession openSession() { 2 return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false); 3 }
調用自身的openSessionFromDataSource方法:
-
getDefaultExecutorType()默認是SIMPLE。
-
注意TX等級是 Null, autoCommit是false。
1 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { 2 Transaction tx = null; 3 4 DefaultSqlSession var8; 5 try { 6 Environment environment = this.configuration.getEnvironment(); 7 // 根據Configuration的Environment屬性來創建事務工廠 8 TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment); 9 // 從事務工廠中創建事務,默認等級為null,autoCommit=false 10 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); 11 // 創建執行器 12 Executor executor = this.configuration.newExecutor(tx, execType); 13 // 根據執行器創建返回對象 SqlSession 14 var8 = new DefaultSqlSession(this.configuration, executor, autoCommit); 15 } catch (Exception var12) { 16 this.closeTransaction(tx); 17 throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12); 18 } finally { 19 ErrorContext.instance().reset(); 20 } 21 return var8; 22 }
構建步驟:Environment>>TransactionFactory+autoCommit+tx-level>>Transaction+ExecType>>Executor+Configuration+autoCommit>>SqlSession
其中,Environment是Configuration中的屬性。
4. 調用Executor執行數據庫操作&&生成具體SQL指令
在拿到SqlSession對象后,我們調用它的insert方法。
1 public int insert(String statement, Object parameter) { 2 3 return this.update(statement, parameter); 4 5 }
它調用了自身的update(statement, parameter)方法:
1 public int update(String statement, Object parameter) { 2 int var4; 3 try { 4 this.dirty = true; 5 MappedStatement ms = this.configuration.getMappedStatement(statement); 6 // wrapCollection(parameter)判斷 param對象是否是集合 7 var4 = this.executor.update(ms, this.wrapCollection(parameter)); 8 } catch (Exception var8) { 9 throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8); 10 } finally { 11 ErrorContext.instance().reset(); 12 } 13 14 return var4; 15 }
mappedStatements就是我們平時說的sql映射對象.
源碼如下:protected final Map<String, MappedStatement> mappedStatements;
可見它是一個Map集合,在我們加載xml配置的時候,mapping.xml的namespace和id信息就會存放為mappedStatements的key,對應的,sql語句就是對應的value.
然后調用BaseExecutor中的update方法:
1 public int update(MappedStatement ms, Object parameter) throws SQLException { 2 ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); 3 if (this.closed) { 4 throw new ExecutorException("Executor was closed."); 5 } else { 6 this.clearLocalCache(); 7 // 真正做執行操作的方法 8 return this.doUpdate(ms, parameter); 9 } 10 }
doUpdate才是真正做執行操作的方法:
1 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { 2 Statement stmt = null; 3 4 int var6; 5 try { 6 Configuration configuration = ms.getConfiguration(); 7 // 創建StatementHandler對象,從而創建Statement對象 8 StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null); 9 // 將sql語句和參數綁定並生成SQL指令 10 stmt = this.prepareStatement(handler, ms.getStatementLog()); 11 var6 = handler.update(stmt); 12 } finally { 13 this.closeStatement(stmt); 14 } 15 16 return var6; 17 }
先來看看prepareStatement方法,看看mybatis是如何將sql拼接合成的:
1 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { 2 Connection connection = this.getConnection(statementLog); 3 // 准備Statement 4 Statement stmt = handler.prepare(connection); 5 // 設置SQL查詢中的參數值 6 handler.parameterize(stmt); 7 return stmt; 8 }
來看看parameterize方法:
1 public void parameterize(Statement statement) throws SQLException { 2 3 this.parameterHandler.setParameters((PreparedStatement)statement); 4 5 }
這里把statement轉換程PreparedStatement對象,它比Statement更快更安全。
這都是我們在JDBC中熟用的對象,就不做介紹了,所以也能看出來Mybatis是對JDBC的封裝。
從ParameterMapping中讀取參數值和類型,然后設置到SQL語句中:
1 public void setParameters(PreparedStatement ps) { 2 ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); 3 List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); 4 if (parameterMappings != null) { 5 for(int i = 0; i < parameterMappings.size(); ++i) { 6 ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); 7 if (parameterMapping.getMode() != ParameterMode.OUT) { 8 String propertyName = parameterMapping.getProperty(); 9 Object value; 10 if (this.boundSql.hasAdditionalParameter(propertyName)) { 11 value = this.boundSql.getAdditionalParameter(propertyName); 12 } else if (this.parameterObject == null) { 13 value = null; 14 } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { 15 value = this.parameterObject; 16 } else { 17 MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); 18 value = metaObject.getValue(propertyName); 19 } 20 21 TypeHandler typeHandler = parameterMapping.getTypeHandler(); 22 JdbcType jdbcType = parameterMapping.getJdbcType(); 23 if (value == null && jdbcType == null) { 24 jdbcType = this.configuration.getJdbcTypeForNull(); 25 } 26 27 try { 28 typeHandler.setParameter(ps, i + 1, value, jdbcType); 29 } catch (TypeException var10) { 30 throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); 31 } catch (SQLException var11) { 32 throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var11, var11); 33 } 34 } 35 } 36 } 37 38 }
5. 對查詢結果二次封裝
在doUpdate方法中,解析生成完新的SQL后,然后執行var6 = handler.update(stmt);我們來看看它的源碼。
1 public int update(Statement statement) throws SQLException { 2 PreparedStatement ps = (PreparedStatement)statement; 3 // 執行sql 4 ps.execute(); 5 // 獲取返回值 6 int rows = ps.getUpdateCount(); 7 Object parameterObject = this.boundSql.getParameterObject(); 8 KeyGenerator keyGenerator = this.mappedStatement.getKeyGenerator(); 9 keyGenerator.processAfter(this.executor, this.mappedStatement, ps, parameterObject); 10 return rows; 11 }
因為我們是插入操作,返回的是一個int類型的值,所以這里mybatis給我們直接返回int。
如果是query操作,返回的是一個ResultSet,mybatis將查詢結果包裝程ResultSetWrapper類型,然后一步步對應java類型賦值等…有興趣的可以自己去看看。
6. 提交與事務
最后,來看看commit()方法的源碼。
1 public void commit() { 2 3 this.commit(false); 4 5 }
調用其對象本身的commit()方法:
1 public void commit(boolean force) { 2 try { 3 // 是否提交(判斷是提交還是回滾) 4 this.executor.commit(this.isCommitOrRollbackRequired(force)); 5 this.dirty = false; 6 } catch (Exception var6) { 7 throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + var6, var6); 8 } finally { 9 ErrorContext.instance().reset(); 10 } 11 }
如果dirty是false,則進行回滾;如果是true,則正常提交。
1 private boolean isCommitOrRollbackRequired(boolean force) { 2 return !this.autoCommit && this.dirty || force; 3 }
調用CachingExecutor的commit方法:
1 public void commit(boolean required) throws SQLException { 2 this.delegate.commit(required); 3 this.tcm.commit(); 4 }
調用BaseExecutor的commit方法:
1 public void commit(boolean required) throws SQLException { 2 if (this.closed) { 3 throw new ExecutorException("Cannot commit, transaction is already closed"); 4 } else { 5 this.clearLocalCache(); 6 this.flushStatements(); 7 if (required) { 8 this.transaction.commit(); 9 } 10 11 } 12 }
最后調用JDBCTransaction的commit方法:
1 public void commit() throws SQLException { 2 if (this.connection != null && !this.connection.getAutoCommit()) { 3 if (log.isDebugEnabled()) { 4 log.debug("Committing JDBC Connection [" + this.connection + "]"); 5 } 6 // 提交連接 7 this.connection.commit(); 8 } 9 }
Demo參考文檔
http://www.mybatis.org/mybatis-3/zh/getting-started.html
最后,歡迎關注下方公眾號:后端技術精選。每天一篇優質技術好文!
