在上一篇 Mybatis源碼-解析配置檔案 文章中介紹了配置檔案中的其他節點,本篇将介紹Mybatis解析Mapper。當解析到mappers的節點時,就能擷取到Maper檔案,進入到mapperElement()方法中開始解析mapper檔案
// 通過解析 mappers 節點,找到Mapper檔案
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 檢視mappers節點中是否有 package 節點,有就解析,否則就解析 mapper子節點
/*
<mappers>
<package name="com.test"/>
<!-- <mapper resource="mapper/DemoMapper.xml"/> -->
</mappers>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 解析 mapper 節點,mapper 節點中有三個屬性(resource、url、class),但是隻能存在一個
/**
* resource:表示檔案夾下xml檔案
* <mapper resource="mapper/DemoMapper.xml"/>
*
* class:DemoMapper 動态代理接口,DemoMapper.xml檔案
* <mapper class="com.test.DemoMapper" />
*
* url:表示盤符下的絕對路徑,絕對不推薦使用
* <mapper resource="F:\javaEE\workspace\MyBatisDemo_My\mybatis-3-master\src\main\resources\mapper\DemoMapper.xml"/>
*
*/
/*
<mappers>
<!-- <package name="com.test"/> -->
<mapper resource="mapper/DemoMapper.xml"/>
</mappers>
*/
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 三個屬性隻能是其中的一個有value,否則就抛異常
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 如果resource有值,就解析mapper.xml檔案
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 如果url有值,則通過絕對路徑擷取輸入流,擷取mapper.xml并解析
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 如果class有值,就擷取Class對象
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.");
}
}
}
}
}
在mappers下的mapper子節點有三個屬性(resource、url、class),但是隻能存在一個,不推薦使用url,如果配置的是class屬性,就擷取Class對象。如果是resource或者url,就要找到這個mapper.xml檔案,開始解析這個mapper.xml檔案裡的節點,進入XMLMapperBuilder裡的parse()方法。
public void parse() {
// 判斷mapper.xml檔案有沒有解析過,如果解析過了就不再解析
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper.xml檔案裡的節點
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
// 解析mapper.xml檔案裡的節點
// 将配置裡面的配置項封裝成一個MapperStatement對象
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"));
// 解析cache節點(擷取cache節點裡的屬性)
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析動态sql節點
/*
<sql id="user_column">
id, username,nickname,...
</sql>
*/
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增删改查節點
/*
<insert id=""/>
<update id=""/>
<delete id=""/>
<select id=""/>
*/
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
一直往下走,進入到XMLStatementBuilder裡的parseStatementNode()方法。
// #{}和${}
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 其實資料庫中分為兩種操作:查詢和更新(增删改)資料庫
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 是否重新整理緩存,預設值:查詢不重新整理,增删改重新整理,可以設定是否重新整理緩存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否使用二級緩存,預設值:查詢使用,增删改不使用
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//是否需要處理嵌套查詢結果,如果有 group by
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 替換使用動态sql的時候使用的<include/>标簽
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
// 拿到參數類型的别名去擷取實際的參數類型對象
Class<?> parameterTypeClass = resolveClass(parameterType);
// 解析配置的自定義腳本語言的驅動,例如可以自定義的去解析sql語句中#{}或者${}的占位符格式
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
// 解析selectKey
/*
<insert id="">
<selectKey></selectKey>
</insert>
*/
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 解析和删除<selectKey>和<include>
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// id的自增規則
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 解析sql語句,根據sql文本來判斷是否需要動态解析,如果沒有動态sql且隻有#{}的時候,就直接解析使用?替換#{},當有${}的時候就不解析
// XMLLanguageDriver.createSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
判斷mapper.xml檔案中的sql語句裡含有的是#{}或者KaTeX parse error: Expected 'EOF', got '#' at position 16: {},如果沒有動态sql且隻有#̲{}的時候,就直接解析使用?替…{}的時候就不解析,這裡需要進入XMLLanguageDriver的createSqlSource()方法裡對sql語句進行操作。
public SqlSource parseScriptNode() {
// 動态解析标簽
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 如果sql文本中含有${,isDynamic的值就為true
if (isDynamic) {
// 不解析
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 用占位符的方式來解析(例如:将#{id}替換成?)
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
進入到parseDynamicTags()方法中。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// sql是純文字
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 判斷sql語句中是否有${},如果有就傳回true
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
繼續往下走到TestSqlNode裡的isDynamoic()方法,判斷sql中是否包含${}
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// 建立一個檢驗規則,裡面含有${}
GenericTokenParser parser = createParser(checker);
// 解析sql文本
parser.parse(text);
return checker.isDynamic();
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
// 判斷sql文本中是否有${或者#{,沒有就傳回-1
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 如果sql文本中含有${,VariableTokenHandler.handleToken将${參數}傳回
// 如果sql文本中含有${,DynamicCheckerTokenParser.handleToken将isDynamic置為true
// 如果sql文本中含有${,BindingTokenParser.handleToken解析sql
// 如果sql文本中含有#{,ParameterMappingTokenHandler.handleToken就傳回 ?
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
當解析完這一步後,mapper檔案差不多就解析完了,解析到的資料會被封裝到MapperStatement對象中,這個對象是被mybatis全局共享的。
當解析完mapper.xml檔案後,當調用mapper裡的方法時,它的執行流程又是怎麼樣的呢?先來看一個Mybatis的一個例子
Test類
public class TestMybatis {
public static void main(String[] args) throws IOException {
// 加載配置檔案
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
// 解析xml檔案
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
// 擷取SqlSession對象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 通過動态代理,拿到DemoMapper的一個代理對象
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
// 通過代理對象執行方法,就會跳到到MapperProxy的invoke方法中
System.out.println(mapper.selectUser(1, "zhangsan"));
}
}
DemoMapper接口
public interface DemoMapper {
Map<String, Object> selectUser(@Param("id") int id, @Param("username") String username);
}
DemoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.test.DemoMapper">
<!-- 不使用@Param-->
<!--<select id="selectUser" resultType="map">
SELECT * FROM user WHERE id=#{arg0} AND username=#{arg1}
</select>-->
<select id="selectUser" resultType="map">
SELECT * FROM user WHERE id=#{id} AND username=#{username}
</select>
</mapper>
當解析完配置檔案,擷取到SqlSessionFacatory對象,通過SqlSessionFacatory對象擷取SqlSession對象,調用getMapper()方法,擷取到DemoMapper對象。但是DemoMapper是一個接口,是不能建立對象的,這裡其實是通過動态代理擷取到一個DemoMapper的子類對象。進入getMapper()方法中。
SqlSession有兩個子類DefaultSqlSession和SqlSessionmanager,點選進入到DefaultSqlSession中,一直進入到MapperRegistry類中的getMapper()方法。
這裡有個(MapperProxyFactory) knownMappers.get(type);方法,這個方法傳回的結果是一個MapperProxyFactory(代理工廠),在來看看knownMappers的定義
knownMappers是一個Map集合,既然這裡能get,那麼肯定就會在一個地方put,在MapperRegistry類裡的addMapper()方法裡進行了knownMappers.put()
addMaper()方法的調用者就需要回到之前配置檔案解析mappers節點的時候,前面已經分析了,解析配置檔案的各個配置的參數會被封裝到Configuration對象中,當解析mapper.xml檔案時,也會調用Configuration的addMapper()方法将mapper的資訊封裝到MapperRegistry中。在XMLMapperBuilder的mapperElement()方法裡可以找到addMapper()方法的調用。
繼續往下執行mapperProxyFactory.newInstance(sqlSession)方法,會發現一個MapperProxy對象,這個就是DemoMapper的代理對象,進入到MapperProxy裡面有一個invoke()方法,當DemoMapper調用它裡面的方法的時候,就會進入這invoke()方法。
// 代理方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 檢視這個方法是否是Object中的方法
/*
Object是所有類的父類,其它類都顯式或者隐式的繼承了這個類,
如果子類中自己定義了方法或者重寫了父類中的方法(例如重寫了toString、equals等),那麼這些方法就不屬于Object了。
隻有Object類中自己定義且沒有被子類重寫的方法,這裡的判斷才屬于Object中的方法
*/
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
// 判斷是否是預設方法,在jdk1.8出現的,在接口中定義的:default void test();
// 如果是預設方法,就直接綁定到代理對象上去執行,預設方法裡該是什麼邏輯就是什麼邏輯,就不進行代理(不執行sql)
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 将method需要的資訊封裝成一個MapperMethod,然後添加到緩存裡
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 執行CRUD操作
return mapperMethod.execute(sqlSession, args);
}
首先會判斷需要執行的方法是否是Object類裡的方法。Object是所有類的父類,其它類都顯式或者隐式的繼承了這個類,如果子類中自己定義了方法或者重寫了父類中的方法(例如重寫了toString、equals等),那麼這些方法就不屬于Object了。隻有Object類中自己定義且沒有被子類重寫的方法,這裡的判斷才屬于Object中的方法。
接着判斷是否是DemoMapper中的預設方法,在JDK8以後在接口中可以出現預設方法,使用default修飾的方法。
default String test() {
return "hello world";
}
如果是預設方法,就直接綁定到代理對象上去執行,預設方法裡該是什麼邏輯就是什麼邏輯,就不進行代理(不執行sql)。
如果該方法不是Object中的方法,也不是預設方法,就繼續往下執行,先去 Map<Method, MapperMethod> methodCache中查詢該method是否存在了,如果沒有就将method封裝成MapperMethod,然後緩存進mehtodCache中,最後将MapperMethod傳回。
往下就執行MapperMethod中的execute方法(其實就是執行CRUD操作),execute()方法下一篇會進行介紹。
公衆号二維碼