本文:3126字 | 閱讀時長:4分10秒mysql
今天是Mybatis源碼分析第四篇,也是最後一篇。sql
老規矩,先上案例代碼:數據庫
public class MybatisApplication {
public static final String URL = "jdbc:mysql://localhost:3306/mblog";
public static final String USER = "root";
public static final String PASSWORD = "123456";
public static void main(String[] args) {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
SqlSession sqlSession = null;
try {
inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//下面這行代碼就是今天的重點
User user = userMapper.selectById(1));
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
sqlSession.close();
}
}
已經分享了三篇Mybatis源碼分析文章,從 Mybatis的配置文件解析 到 獲取SqlSession,再到 獲取UserMapper接口的代理對象。設計模式
今天咱們來分析,userMapper中的方法和UserMapper.xml中的SQL是怎麼關聯的,以及怎麼執行SQL的。緩存
咱們的故事從這一行代碼開始:數據結構
User user = userMapper.selectById(1));
這一行代碼背後源碼搞完,也就表明着咱們Mybatis源碼搞完(主幹部分)。mybatis
下面咱們繼續開擼。app
在上一篇中咱們知道了userMapper是JDK動態代理對象,因此調用這個代理對象的任意方法都是執行觸發管理類MapperProxy的invoke()方法。ide
因爲篇幅較長,爲了更好閱讀,這裏把文章分紅兩個部分:源碼分析
第一部分流程圖:
開篇已經說過了,調用userMapper的方法就是調用MapperProxy的invoke()方法,因此咱們就從這invoke()方法開始。
若是對於Mybatis源碼不是很熟悉的話,建議先看看前面的文章。
//MapperProxy類
@Override
public Object invoke(....) throws Throwable {
try {
//首先判斷是否爲Object自己的方法,是則不須要去執行SQL,
//好比:toString()、hashCode()、equals()等方法。
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
//判斷是否JDK8及之後的接口默認實現方法。
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//<3>
final MapperMethod mapperMethod = cachedMapperMethod(method);
//<4>
return mapperMethod.execute(sqlSession, args);
}
<3>處是從緩存獲取MapperMethod,這裏加入了緩存主要是爲了提高MapperMethod的獲取速度。緩存的使用在Mybatis中也是很是之多。
private final Map<Method, MapperMethod> methodCache;
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
Map的computeIfAbsent方法:根據key獲取值,若是值爲null,則把後面的Object的值付給key。
繼續看MapperMethod這個類,定義了兩個屬性command和method,兩個屬性與之相對應的兩個靜態內部類。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(...) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
//得到 MappedStatement 對象
MappedStatement ms = resolveMappedStatement(...);
// <2> 找不到 MappedStatement
if (ms == null) {
// 若是有 @Flush 註解,則標記爲 FLUSH 類型
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
// 拋出 BindingException 異常,若是找不到 MappedStatement
//(開發中容易見到的錯誤)說明該方法上,沒有對應的 SQL 聲明。
throw new BindingException("Invalid bound statement (not found): "+ mapperInterface.getName() + "." + methodName);
}
} else {
// 得到 name
//id=com.tian.mybatis.mapper.UserMapper.selectById
name = ms.getId();
// 得到 type=SELECT
type = ms.getSqlCommandType();
//若是type=UNKNOWN
// 拋出 BindingException 異常,若是是 UNKNOWN 類型
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(...) {
// 得到編號
//com.tian.mybatis.mapper.UserMapper.selectById
String statementId = mapperInterface.getName() + "." + methodName;
//若是有,得到 MappedStatement 對象,並返回
if (configuration.hasStatement(statementId)) {
//mappedStatements.get(statementId);
//解析配置文件時候建立並保存Map<String, MappedStatement> mappedStatements中
return configuration.getMappedStatement(statementId);
// 若是沒有,而且當前方法就是 declaringClass 聲明的,則說明真的找不到
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 遍歷父接口,繼續得到 MappedStatement 對象
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(...);
if (ms != null) {
return ms;
}
}
}
// 真的找不到,返回 null
return null;
}
//....
}
public static class MethodSignature {
private final boolean returnsMap;
private final Class<?> returnType;
private final Integer rowBoundsIndex;
//....
}
SqlCommand封裝了statement ID,好比說:
com.tian.mybatis.mapper.UserMapper.selectById
和SQL類型。
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}
另外,還有個屬性MethodSignature方法簽名,主要是封裝的是返回值的類型和參數處理。這裏咱們debug看看這個MapperMethod對象返回的內容和咱們案例中代碼的關聯。
妥妥的,故事繼續,咱們接着看MapperMethod中execute方法。
上面代碼中<4>處,先來看看這個方法的總體邏輯:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT:
//部分代碼省略....
Object param = method.convertArgsToSqlCommandParam(args);
//本次是QUERY類型,因此這裏是重點
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
這個方法中,根據咱們上面得到的不一樣的type(INSERT、UPDATE、DELETE、SELECT)和返回類型:
(本文是查詢,因此這裏的type=SELECT)
調用convertArgsToSqlCommandParam()將方法參數轉換爲SQL的參數。
2.調用selectOne()方法。這裏的sqlSession就是DefaultSqlSession,因此咱們繼續回到DefaultSqlSession中selectOne方法中。
繼續DefaultSqlSession中的selectOne()方法:
//DefaultSqlSession中
@Override
public <T> T selectOne(String statement, Object parameter) {
//這是一種好的設計方法
//無論是執行多條查詢仍是單條查詢,都走selectList方法(重點)
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
//若是隻有一條就返回第一條
return list.get(0);
} else if (list.size() > 1) {
//(開發中常見錯誤)方法定義的是返回一條數據,結果查出了多條數據,就會報這個異常
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
//數據庫中沒有數據就返回null
return null;
}
}
這裏調用的是本類中selectList方法。
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//從configuration獲取MappedStatement
//此時的statement=com.tian.mybatis.mapper.UserMapper.selectById
MappedStatement ms = configuration.getMappedStatement(statement);
//調用執行器中的query方法
return executor.query(...);
} catch (Exception e) {
//.....
} finally {
ErrorContext.instance().reset();
}
}
在這個方法裏是根據statement從configuration對象中獲取MappedStatement對象。
MappedStatement ms = configuration.getMappedStatement(statement);
在configuration中getMappedStatement方法:
//存放在一個map中的
//key是statement=com.tian.mybatis.mapper.UserMapper.selectById,value是MappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>();
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
return mappedStatements.get(id);
}
而MappedStatement裏面有xml中增刪改查標籤配置的全部屬性,包括id、statementType、sqlSource、入參、返回值等。
到此,咱們已經將UserMapper類中的方法和UserMapper.xml中的sql給完全關聯起來了。繼續調用executor中query()方法:
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
這裏調用的是執行器Executor中的query()方法。
第二部分流程:
這裏的Executor對象是在調用openSession()方法時建立的。關於這一點咱們在前面的文章已經說過,這裏就再也不贅述了。
下面來看看調用執行器的query()放的整個流程:
咱們股市繼續,看看具體源碼是如何實現的。
CachingExecutor.query()
在CachingExecutor中
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
BoundSql中主要是SQL和參數:
既然是緩存,咱們確定想到key-value數據結構。
下面來看看這個key生成規則:
這個二級緩存是怎麼構成的呢?而且還要保證在查詢的時候必須是惟一。
也就說,構成key主要有:
方法相同、翻頁偏移量相同、SQL相同、參數相同、數據源環境相同纔會被認爲是同一個查詢。
這裏能說到這個層面就已經闊以了。
若是向更深刻的搞,就得把hashCode這些扯進來了,請看上面這個張圖裏前面的幾個屬性就知道和hashCode有關係了。
處理二級緩存
首先是從ms中取出cache對象,判斷cache對象是否爲null,若是爲null,則沒有查詢二級緩存和寫入二級緩存的流程。
有二級緩存,校驗是否使用此二級緩存,再從事務管理器中獲取二級緩存,存在緩存直接返回。不存在查數據庫,寫入二級緩存再返回。
@Override
public <E> List<E> query(....)
throws SQLException {
Cache cache = ms.getCache();
//判斷是否有二級緩存
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
那麼這個Cache對象是什麼建立的呢?
在解析UserMapper.xml時候,在XMLMapperBuilder類中的cacheElement()方法裏。
關於二級緩存相關這一塊在前面文章已經說過,好比:
解析上面這些標籤
建立Cache對象:
二級緩存處理完了,就來到BaseExecutor的query方法中。
第一步,清空緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
queryStack用於記錄查詢棧,防止地櫃查詢重複處理緩存。
flushCache=true的時候,會先清理本地緩存(一級緩存)。
若是沒有緩存會從數據庫中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
在看看這個方法的邏輯:
private <E> List<E> queryFromDatabase(...) throws SQLException {
List<E> list;
//使用佔位符的方式,先搶佔一級緩存。
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//刪除上面搶佔的佔位符
localCache.removeObject(key);
}
//放入一級緩存中
localCache.putObject(key, list);
return list;
}
先在緩存使用佔位符佔位,而後查詢,移除佔位符,將數據放入一級緩存中。
執行Executor的doQuery()方法,默認使用SimpleExecutor。
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
下面就來到了SimpleExecutor中的doQuery方法。
@Override
public <E> List<E> doQuery(....) throws SQLException {
Statement stmt = null;
try {
//獲取配置文件信息
Configuration configuration = ms.getConfiguration();
//獲取handler
StatementHandler handler = configuration.newStatementHandler(....);
//獲取Statement
stmt = prepareStatement(handler, ms.getStatementLog());
//執行RoutingStatementHandler的query方法
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
建立StatementHandler
在configuration中newStatementHandler()裏,建立了一個StatementHandler對象,先獲得RoutingStatementHandler(路由)。
public StatementHandler newStatementHandler() {
StatementHandler statementHandler = new RoutingStatementHandler();
//執行StatementHandler類型的插件
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
RoutingStatementHandler建立的時候是就是建立基本的StatementHandler對象。
這裏會根據MapperStament裏面的statementType決定StatementHandler類型。默認是PREPARED。
StatementHandler裏面包含了處理參數的ParameterHandler和處理結果集的ResultHandler。
上面說的這幾個對象正式被插件攔截的四大對象,因此在建立的時都要用攔截器進行包裝的方法。
對於插件相關的,請看以前已發的插件文章:插件原理分析。
咱們故事繼續:
建立對象後就會執行RoutingStatementHandler的query方法。
//RoutingStatementHandler中
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//委派delegate=PreparedStatementHandler
return delegate.query(statement, resultHandler);
}
這裏設計頗有意思,全部的處理都要使用RoutingStatementHandler來路由,所有經過委託的方式進行調用。
而後執行到PreparedStatementHandler中的query方法。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//JDBC的流程了
ps.execute();
//處理結果集,若是有插件代理ResultHandler,會先走到被攔截的業務邏輯中
return resultSetHandler.handleResultSets(ps);
}
看到了ps.execute(); 表示已經到JDBC層面了,這時候SQL就已經執行了。後面就是調用DefaultResultSetHandler類進行結果集處理。
到這裏,SQL語句就執行完畢,並將結果集賦值並返回了。
總算搞完把Mybatis主幹源碼掠了一遍,鬆口氣~,能看到這裏,證實小夥伴也是蠻用心的,辛苦了。越努力越幸福!
建議抽時間,再次debug,還有就是畫畫類之間的關係圖,還有就是Mybatis中設計模式好好回味回味。
從調用userMapper的selectById()方法開始,到方法和SQL關聯起來,參數處理,再到JDBC中SQL執行。
完整流程圖:
感興趣的小夥伴,能夠對照着這張流程圖就行一步一步的debug。
記着:努力的人,世界不會虧待你的。