1 總結
代碼有些多,怕有的大兄弟耐不下心, 就先寫一個總結。
mybatis的的大概流程是這樣的:
- 通過解析配置檔案分析mapper檔案和接口,生成代理對象。
- 根據配置檔案,建立會話
- 通過會話拿到代理對象
- 通過代理對象,執行具體方法,将接口和sql關聯,并執行。
Note: mybatis version --3.4.6
2 代碼示例
public void howToUseMybatis() throws Exception {
String confLocation = "mybatis-conf.xml";
Reader reader = Resources.getResourceAsReader(confLocation);
// step1. 通過配置檔案建構 SqlSessionFactory
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
// step2. openSession
SqlSession sqlSession = sessionFactory.openSession();
// step3. 給Mapper接口生成對應的的代理類。
PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
// step4. 調用方法
Pet pet = petMapper.selectByName("zhangsan");
System.out.println(pet);
}
2.1 Step1
解析配置檔案,然後建構出SqlSessionFactory。build中有很多細節,需要一一分析出來。
從
org.apache.ibatis.session.SqlSessionFactoryBuilder#build()
方法一直點下, 進入終極buiild。在終極build中,有parse方法,顧名思義,那就是解析,那就要去看去解析什麼。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 進入parse方法。
return build(parser.parse());
}
..
}
進入
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
方法, 可以看出來,是在解析的配置檔案,根節點是configuration。
public Configuration parse() {
...
// 具體解析
parseConfiguration(parser.evalNode("/configuration"));
// 傳回的是configration對象。
return configuration;
}
接下來,進入
parseConfiguration
,繼續追蹤細節。
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mapper檔案,我們需要具體關注。因為關乎後面如何通過接口代理來調用方法。
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我們通過節點名稱可以看出來,在解析configuration檔案中的每個節點。但裡面有個我們需要特别關注,那就是
mapperElement(root.evalNode("mappers"));
,這個關乎後面如何和java代理關聯起來。通過接口來調用方法。我們進入這個方法,檢視具體細節。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
...
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 因為我配置是resource,是以進入的是此方法。
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
configuration.addMapper(mapperInterface);
} else {
}
}
}
}
這個方法裡面是根據配置檔案,采用什麼邏輯來解析,因為我配置的mapper-resources, 是以進入的是上面方法。沒啥好逼逼的,直接點進去看看。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper配置檔案
configurationElement(parser.evalNode("/mapper"));
// 防止重複解析
configuration.addLoadedResource(resource);
// 關聯mapper.xml對應的的mapper.java檔案
bindMapperForNamespace();
}
...
}
2.1.1 解析mapper.xml
來了,就是這, 就是這!!讓我開始裸看代碼出現迷糊的地方,沒有找到解析配置檔案的地方。大家注意,我們一步步來。先從
configurationElement(parser.evalNode("/mapper"));
看起來。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 需要關注這個,這個就是解析sql的方法入口
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
}
}
我們通過解析的節點可以看出來,是在解析配置中的節點。例如:resultMap,sql等。 我們需要關注
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
,因為這個是具體解析sql的方法。并且透露一下,這個裡面也是生成MappedStatement的地方。
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// look here. 具體執行的地方。
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
我把兩個代碼一起貼出來,這兩段代碼沒什麼好解釋的,我們直接看
statementParser.parseStatementNode();
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
...
// MappedStatement
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
代碼比較多,删了一部分。不過我們還是可以看出來,就是在解析每一個sql片段,生成一個MappedStatement. 不過我們還是要點進
addMappedStatement
方法, 因為裡面還有一段生成id的邏輯。
public MappedStatement addMappedStatement(...){
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
// 處理ID, 把id轉換成使用namespace+方法名。
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
我們在這就可以清楚的看到,mapper.xml檔案最後被解析成了MappedStatement檔案,并且id是以namespace+方法名組成的。并且把這些MappedStatement放入到了全局的configuration類中。自此,我們看到MappedStatement細節全部處理完成。
2.1.2 解析Mapper.java
回過頭,我們接着看
bindMapperForNamespace();
方法,這個方法也比較重要。也解釋了為什麼明明是接口(Interface), 但是可以傳回對象。Let's go .
private void bindMapperForNamespace() {
// 擷取MappedStatement 的Namespace
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 通過namespace擷取接口類型。這裡也說明了,為什麼我們在配置mapper.xml檔案的時候,
// namespace 要和接口的包名相同
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 關鍵點,把mapper.java類型儲存起來
configuration.addMapper(boundType);
}
}
}
核心點了,解析到Mapper.java後,調用了
configuration.addMapper(boundType);
,裡面具體是什麼樣的呢?我們一路點下去,最後addMapper的具體代碼如下.
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 {
// 核心關鍵點,把類型和,和類型生成的MapperProxyFactory放入了map中儲存了起來
knownMappers.put(type, new MapperProxyFactory<T>(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);
// 通過接口,解析,因為有的sql直接寫在接口上面。這樣的也會生成MappedStatement對象。
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
我們看到,核心是把類型,和代理工廠放入到了Map中儲存了起來。接着解析接口上面的sql。至此。build代碼解析完了,核心關鍵點都點了出來。但是還有一個小細節,我們看到解析完,傳回的configuration對象。然後通過buid()方法,build出來的是DefaultSqlSessionFactory。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
2.2 Step2
從openSession()方法點進去,直接看看是如何開啟一個會話的。(上面說到了,生成的是DefaultSqlSessionFactory, 别進錯了)
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 擷取資料庫資訊,已經解析完。
final Environment environment = configuration.getEnvironment();
// 生成TransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 建立事務
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 建立執行器
final Executor executor = configuration.newExecutor(tx, execType);
// 生成SqlSession
return 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.2.1 Executor
這個裡面有生成SqlSession的具體不走,前面沒啥好看的,就是從解析出來的configuration中擷取資訊。 我們直接從建立執行器開始。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 擷取需要建立的執行器的類型,
executorType = executorType == null ? defaultExecutorType : executorType;
// 有可能預設類型也為空,是以多判斷了一次。
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 如果沒有輸入類型,則預設建立的是 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
// 如果開啟了二級緩存,則會在對執行器進行包裝
executor = new CachingExecutor(executor);
}
// 攔截器連
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
我們可以看到,執行器有三種類型,分别是
BatchExecutor
,
ReuseExecutor
SimpleExecutor
(就不介紹三種執行器了)。預設采用的是SimpleExecutor。
2.2.2 SqlSession
我們看代碼,發現生成
DefaultSqlSession
傳入了Executor,在sqlsession中調用方法時,具體的執行邏輯是通過executor執行的。那一個構造函數和查詢方法看一下。
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 通過執行器來執行查詢
Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
registerCursor(cursor);
return cursor;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
2.3 Step3
通過SqlSession擷取Mapper對象。 這個就得好好說道說道了。明明是接口,為什麼可以傳回對象?我們從代碼中,一直點下去,直接發現代碼。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 從最開始解析儲存接口和代理工廠的map中,擷取對象的MapperProxyFactory
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);
}
}
我們發現getMapper,最後傳回的是代理對象,這也就是為什麼明明是接口,但是可以傳回對象。但是為了明白具體邏輯,我們還是要進去看看具體是如何實作的,以及如何是把MappedStatement和接口關聯起來的。
public T newInstance(SqlSession sqlSession) {
// 建立 MapperProxy ,
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 這是一個InvocationHandler,代理對象具體執行的邏輯
....
@Override
public 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);
}
// 這個裡面有核心,方法是如何和Mapper關聯起來的
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 具體執行邏輯
return mapperMethod.execute(sqlSession, args);
}
}
看到這, 我們應該都明白了,接口調用的方法,通過走代理,然後在代理對象中進行邏輯處理。 這個時候,我們就要進
cachedMapperMethod(method);
,這裡是核心。
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
// 構造MapperMethod 對象, 這個裡面就是具體的關聯邏輯。
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
// MapperMethod的構造函數
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 關聯邏輯
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
這段代碼是建構MapperMethod, 關鍵的邏輯在構造函數裡面。我們來看SqlCommand的構造函數
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 核心邏輯,顧名思義,解析mappedStatement.并且是通過Method.
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
...
}
到這,我們就得就去瞅瞅了,是如何解析MappedStatement的,關聯接口方法和MappedStatement.
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// statementId
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
// 通過statementId擷取存入的mappedStatement
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
// 父類接口。邏輯一樣
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
看到這,就知道了,是通過方法的類名+方法名,組成statementId,然後去早已經解析的MappedStatement中,找到對應的MappedStatment,就關聯起來啦。
2.4 Step4
執行方法。通過上面分析,我們已經知道是通過代理,找到對應的MappedStatement,然後執行具體的sql.
3 完結撒花
ooook, 現在已經搞完全部的流程啦,要自己在捋順啦,鋼巴得!
挂上GitHub代碼位址,拉下來,在本地Debug起來。
你的每一個點贊,我都當做喜歡