面試官:你分析過mybatis工作原理嗎?


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

 

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

  1. 讀取核心配置文件並返回InputStream流對象。

  2. 根據InputStream流對象解析出Configuration對象,然后創建SqlSessionFactory工廠對象

  3. 根據一系列屬性從SqlSessionFactory工廠中創建SqlSession

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

  5. 對執行結果進行二次封裝

  6. 提交與事務

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

 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方法:

  1. getDefaultExecutorType()默認是SIMPLE。

  2. 注意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

其中,EnvironmentConfiguration中的屬性。

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.xmlnamespaceid信息就會存放為mappedStatementskey,對應的,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

 

最后,歡迎關注下方公眾號:后端技術精選。每天一篇優質技術好文!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM