mybatis源码_MyBatis架构和源码
Mybatis架構解讀
1. 架構圖
如題,這就是MyBatis的執行架構圖。解釋一下:我們在使用MyBatis的CRUD操作的時候,一般有兩種方式,一、直接調用sqlSession的crud方法;二、通過調用getMapper獲取到接口代理的實現類,然后在代理方法中調用了crud方法。可以看到,本質相同,最終調用的都是sqlSession的方法,上圖就是CRUD執行的流程
2. 執行流程圖
我們先來看一下我們執行一個MyBatis的查詢,需要做什么。
//加載一個配置文件 InputStream resourceAsStream = Resources.getResourceAsStream("main.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory build = sqlSessionFactoryBuilder.build(resourceAsStream); SqlSession sqlSession = build.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class);//代理模式創建了一個實現類 List<User> all = mapper.findAll(1); all.forEach(System.out::println);這就是一個最簡單的查詢過程。下面我們來分析一下他們每一步做了什么事情。
2.1 Resources.getResourceAsSteam
很簡單,讀取了一個配置文件。可能有的小伙伴這個樣子干過,直接將通過本類的類加載器拿到資源路徑,然后直接獲取這個主配置文件,但提示未找到。
看一下他的源碼,他直接拿了一個系統類加載器。
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);}return in;}ClassLoaderWrapper() {try {systemClassLoader = ClassLoader.getSystemClassLoader();} catch (SecurityException ignored) {// AccessControlException on Google App Engine}}這個時候,我們自己使用ClassLoader獲取系統類加載器加載資源, 這個時候也是可以成功獲取的。于是想到了一個方法我比較了一下本類類加載器和系統類加載的類別,發現都是通過ApplicationClassLoader加載的,但系統類加載器無法加載
后來了解到的原因就是由于Maven插件的原因,在插件的地方指定一個Resource的映射路徑即可,不過建議直接使用MyBatis的加載方式,簡單一點。
2.2 new SqlSessionFactoryBuilder.build
創建了一個SqlSessionFactoryBuilder構建者對象,構建者模式
然后通過build方法加載配置文件的資源。配置文件包括:主配置文件、Mapper文件或者注解。
來看一下我們的主配置文件
<configuration><typeAliases><package name="com.bywlstudio.bean"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><……………………></dataSource></environment></environments><mappers><mapper class="com.bywlstudio.mapper.UserMapper"></mapper></mappers> </configuration>XML文件,MyBatis通過XPath語法進行解析,首先拿了一個Configuration節點,然后再解析內部的節點,每一個節點對應一個方法。看一下源碼
private void parseConfiguration(XNode root) {try {//issue #117 read properties firstpropertiesElement(root.evalNode("properties"));Properties settings = settingsAsProperties(root.evalNode("settings"));loadCustomVfs(settings);loadCustomLogImpl(settings);typeAliasesElement(root.evalNode("typeAliases"));pluginElement(root.evalNode("plugins"));objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631environmentsElement(root.evalNode("environments"));databaseIdProviderElement(root.evalNode("databaseIdProvider"));typeHandlerElement(root.evalNode("typeHandlers"));mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}}接下來要做的事情,就比較清晰了,解析每一個XML標簽中的節點、文本和屬性值,為對應的對象進行封裝
我們主要看一下environments解析做了什么。
要看他做了什么,得先看它有什么。
它內部有多個environment元素,還有最關鍵的信息,事務管理者和數據源
所以在這個方法中他封裝了一個Environment對象,內部存放了一個事務工廠和一個數據源對象。看一下Environment類信息
public final class Environment {private final String id;private final TransactionFactory transactionFactory;private final DataSource dataSource;接下來再看重頭戲Mappers的解析
Mappers中可以存在四種映射方式:面試題
- package。指定一個需要掃描的包
- resource。指定一個本地的mapper映射文件
- url。指定一個url可以為網絡的mapper映射文件
- class。指定一個類作為一個需要被代理的mapper
接下來我們看一下他的處理方式:
private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {//子節點是否為packageif ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");configuration.addMappers(mapperPackage);} else {String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapperClass = child.getStringAttribute("class");//屬性是否為resourceif (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());//xml方式處理mapperParser.parse();//屬性是否為url} 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();//屬性是否為class} else if (resource == null && url == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);//注解的方式處理configuration.addMapper(mapperInterface);} else {throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");}}}}}2.2.1 xml方式
先來聊一下xml的處理方式
首先拿到對應的mapper文件,之后創建了一個解析該資源的類XMLMapperBuilder。解析子標簽mapper等等屬性,邏輯和之前一樣,最后將所有的信息添加到了Configutation類中。
2.2.2 注解方式
一個核心方法org.apache.ibatis.binding.MapperRegistry#addMapper。有一個點,當你的Mapper不是一個接口的時候,他直接不處理了
public <T> void addMapper(Class<T> type) {//是否為接口if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {//將Mapper的信息,封裝到一個MapperProxyFactory工廠中knownMappers.put(type, new MapperProxyFactory<>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);//具體解析parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}}這個方法做的最重要的一件事情:
- 將所有的mapper信息存放到了MapperRegistry#knownMappers集合中
具體的解析過程中,他還設置了StatementType=PREPARED;
后面解析的過程主要進行注解解析,判斷是否存在某某某注解,最后將所有的信息封裝到了一個Configuration中。
每一條SQL對應一個MappedStatement對象,該對象不可變
public final class MappedStatement {private String resource;private Configuration configuration;private String id;private Integer fetchSize;private Integer timeout;private StatementType statementType;private ResultSetType resultSetType;private SqlSource sqlSource;private Cache cache;private ParameterMap parameterMap;private List<ResultMap> resultMaps;private boolean flushCacheRequired;private boolean useCache;private boolean resultOrdered;private SqlCommandType sqlCommandType;private KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;private boolean hasNestedResultMaps;private String databaseId;private Log statementLog;private LanguageDriver lang;private String[] resultSets;2.3 openSession
本質上創建了一個DefalutSqlSession對象。創建了Executor
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();//獲取之前的事務工廠final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);//創建一個事務tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//創建一個執行器final Executor executor = configuration.newExecutor(tx, execType);//創建了一個SQLSessionreturn new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}2.4 getMapper
還記得在build中,Mybatis將mapper信息封裝為一個MapperProxyFactory添加到了一個List中,而現在的GetMapper就從里面拿到對應的Mapper代理工廠信息,然后創建對應的Mapper代理對象,最后返回
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}我們來看一下我們的代理對象
@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {//這個就是我們的代理對象,也就是實現了代理接口的類final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}看一下這個里面有什么
public class MapperProxy<T> implements InvocationHandler, Serializable {private static final long serialVersionUID = -6424540398559729838L;private final SqlSession sqlSession;private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache;//執行增強的具體的方法public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {2.5 具體的CRUD
在getMapper中,我們知道了此時返回的是一個該接口的代理對象,當我們執行具體的方法的時候,就走了其代理方法。
主要執行邏輯是:判斷Sql的操作類型,然后執行對應的方法,如果是查詢,則從緩存中查詢,如果沒有,則查詢數據庫,查到以后,將查詢到的信息進行封裝,封裝以后,將這個信息添加的緩存中,然后返回。
他首先判斷了該方法的類信息是不是object,然后判斷是不是默認方法,如果是分別執行。最后他給本類的methodCache中添加了一個方法映射
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (isDefaultMethod(method)) {return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}接下來再看一下具體的執行方法。
public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT://返回值是否為nullif (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;//返回值是否為多個(List)} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {//返回值是否為鍵值result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {//返回值為一個Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional() &&(result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}return result;}這個時候就回歸到了SqlSession的API調用了
2.6 SqlSession具體調用
拿Select為例
首先他生成了一個cacheKey,拿這個key從緩存中找,如果沒有查詢數據庫,查到以后將對應的結果放到緩存中,然后返回給用戶
//調用函數, executor.query(MappedStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;//查詢緩存list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}- 查詢以后放置到緩存并且返回的操作
- 重頭戲來了,接下來將會創建架構圖里的第二個內容StatementHandler
在創建的時候,他將所有的StatementHandler攔截器都執行了一遍。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//創建了一個StatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);//執行所有的Statement攔截器(所有)statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}還記得在build的時候,指定了一個StatementType.PREPARED類型嗎?這個時候這個東西就開始起作用了。在創建RoutingStatementHandler這個類的時候,他根據StatementType類型創建了一個子類,而現在創建的就是PreparedStatementHandler,而在這個類的父類里創建了ParameterHandler和ResultSetHandler。
public class RoutingStatementHandler implements StatementHandler {private final StatementHandler delegate;public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}他們在創建的時候又將對應的所有攔截器執行了一遍。
到了這里,架構圖里的東西已經全部出來了。
接下來就是執行SQL了
2.7 總結
我們來看一下Configuration類。一家人整整齊齊,圖上的東西都在這里了
整體執行邏輯就是:
- 創建一個Executor對象,將事務和數據源放進去
- 創建StatementHandler實現類,將其對應的攔截器執行了
- 在創建實現類的時候又創建了ParameterHandler實現類,并且將其攔截器執行了
- 同時也創建了ResultSetHandler,并且將其攔截器執行了
- 之后通過這個結果集映射做了一次對象封裝,將數據存到緩存里,然后返回了。
3. 面試題
面試題整理自網絡,方便復習3.1 #{}和${}的區別
- ${}是Properties文件中的變量占位符,可以應用于標簽屬性值和SQL內部,屬于靜態替換
- #{}是SQL參數占位符,MyBatis會將SQL中的#{}替換為?,在SQL執行前通過PreparedStatement的參數設置方法,設置具體的參數值。
3.2 XML映射文件中,除了select|insert|update|delete還有哪些標簽
答:
- <resultMap>。自定義結果集映射
- <cache>。定義當前命名空間中的緩存配置策略
- <cache-ref>。引用其他命名空間的緩存配置
- <sql>。定義一個SQL語句塊,可以被引用
- 動態SQL
- <include>引用一個SQL塊
- <foreach>
- <if>
- <where>
- <trim>
3.3 最佳實踐中,通常一個Xml映射文件,都會寫一個Dao接口與之對應,請問,這個Dao接口的工作原理是什么?Dao接口里的方法,參數不同時,方法能重載嗎?
答:在MyBaits中,每一個命名空間的方法都擁有一個唯一標識。接口全限定類名.方法名,所以參數不同不能重載。其工作原理是通過JDK動態代理實現的。真正執行的是MapperProxy
3.4 MyBatis是如何進行分頁的?分頁插件的原理是什么?
Mybatis使用RowBounds對象進行分頁,它是針對ResultSet結果集執行的內存分頁,而非物理分頁。可以在sql內直接書寫帶有物理分頁的參數來完成物理分頁功能,也可以使用分頁插件來完成物理分頁。
分頁插件的基本原理是使用Mybatis提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的sql,然后重寫sql
3.5 簡述Mybatis的插件運行原理,以及如何編寫一個插件。
答:Mybatis僅可以編寫針對ParameterHandler、ResultSetHandler、StatementHandler、Executor這4種接口的插件,Mybatis使用JDK的動態代理,為需要攔截的接口生成代理對象以實現接口方法攔截功能,每當執行這4種接口對象的方法時,就會進入攔截方法,具體就是InvocationHandler的invoke()方法,當然,只會攔截那些你指定需要攔截的方法。
實現Mybatis的Interceptor接口并重寫intercept()方法,然后在給插件編寫注解,指定要攔截哪一個接口的哪些方法即可,記住,別忘了在配置文件中配置你編寫的插件。
3.6 Mybatis是如何將sql執行結果封裝為目標對象并返回的?都有哪些映射形式?
答:第一種是使用<resultMap>標簽,逐一定義列名和對象屬性名之間的映射關系。第二種是使用sql列的別名功能,將列別名書寫為對象屬性名,比如T_NAME AS NAME,對象屬性名一般是name,小寫,但是列名不區分大小寫,Mybatis會忽略列名大小寫,智能找到與之對應對象屬性名,你甚至可以寫成T_NAME AS NaMe,Mybatis一樣可以正常工作。
有了列名與屬性名的映射關系后,Mybatis通過反射創建對象,同時使用反射給對象的屬性逐一賦值并返回,那些找不到映射關系的屬性,是無法完成賦值的。
3.7 Mybatis能執行一對一、一對多的關聯查詢嗎?都有哪些實現方式,以及它們之間的區別。
答:在Mybatis的ResultMap中可以通過Result標簽或者注解指定需要映射的表。通過內部的one和many實現具體的一對一或者一對多映射關系。
比如可以通過Result中的one實現一對一映射。內部的select指定另一個查詢語句,fetchType用于指定是否使用懶加載
@Results({@Result(id = true , column = "id" , property = "id"),@Result(column = "nickName" , property = "nickName"),@Result(column = "gender" , property = "gender"),@Result(column = "city" , property = "city"),@Result(column = "province" , property = "province"),@Result(column = "wid" , property = "wxuser" ,one = @One(select = "com.bywlstudio.dao.IWXUserDao.findWXUserById" ,fetchType = FetchType.EAGER)),})3.8 懶加載實現原理
答:通過代理方法創建代理對象以后,在真正獲取數據的時候到達攔截器的方法之后,攔截器方法首先判斷當前值是否為null,如果為null,則通過預先的SQL查詢并且set,最后get查詢。
3.9 myBatis如何執行批處理
答:通過BatchExecutor完成批處理
3.10 MyBatis有哪些Executor執行,以及他們之間的區別
答:
- SimpleExecutor,執行一次update或者select就開啟一個statement,用完立刻關閉
- ReuseExecutor,執行update或者select,以SQL作為key查找Statement對象,存在就使用,不存在就創建,用完以后,添加到Map<String,Statement>中
- BatchExecutor,執行update,將所有的Sql添加到批處理中,等待統一執行,緩存了多個Statement對象。
3.11 Mybatis中如何指定使用哪一種Executor執行器?
在Mybatis配置文件中,可以指定默認的ExecutorType執行器類型,也可以手動給DefaultSqlSessionFactory的創建SqlSession的方法傳遞ExecutorType類型參數。
3.12 Mybatis是否可以映射Enum枚舉類?
Mybatis可以映射枚舉類,不單可以映射枚舉類,Mybatis可以映射任何對象到表的一列上。映射方式為自定義一個TypeHandler,實現TypeHandler的setParameter()和getResult()接口方法。TypeHandler有兩個作用,一是完成從javaType至jdbcType的轉換,二是完成jdbcType至javaType的轉換,體現為setParameter()和getResult()兩個方法,分別代表設置sql問號占位符參數和獲取列查詢結果。
往期回顧
撩改JVM常見調優參數?mp.weixin.qq.com入門JVM?讀這一篇就夠了?mp.weixin.qq.com多線程知識點小結?mp.weixin.qq.comLock和Synchronized?mp.weixin.qq.com你了解線程池嗎??mp.weixin.qq.com總結
以上是生活随笔為你收集整理的mybatis源码_MyBatis架构和源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: axios get传参_axios 传数
- 下一篇: mybatis mapper文件找不到_