Mybatis原始碼簡單解讀----構建
Mybatis原始碼簡單解讀—構建
參考原始碼:Github搜尋mybatis原始碼第一個,中文註釋
同時maven下載mybatis較新版本的原始碼對照閱讀
參考部落格:https://www.cnblogs.com/javazhiyin/p/12340498.html
其他的小知識點也借鑑了很多其他部落格的內容
首先mybatis的工作流程主要分為兩個部分:
- 構建(解析xml和註解,對映成物件形成配置類)
- 執行(執行sql,完成jdbc與資料庫互動)
這一部分只講解mybatis的構建
單獨使用mybatis框架的時候,會需要兩個配置檔案,分別是mybatis-config.xml和mapper.xml,採用官網給出案例mybatis的xml
我們不難看出,在mybatis-config.xml這個檔案主要是用於配置資料來源、配置別名、載入mapper.xml,並且我們可以看到這個檔案的<mappers>
節點中包含了一個<mapper>
,而這個mapper所指向的路徑就是另外一個xml檔案:DemoMapper.xml,而這個檔案中寫了我們查詢資料庫所用的SQL。
而,MyBatis實際上就是將這兩個xml檔案,解析成配置物件,在執行中去使用它。
xml配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
<?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="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
使用mybatis,呼叫程式碼如下:
public static void main(String[] args) throws Exception {
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//建立SqlSessionFacory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
/******************************分割線******************************/
SqlSession sqlSession = sqlSessionFactory.openSession();
//獲取Mapper
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
sqlSession.commit();
}
這段程式顯示通過位元組流讀取了mybatis-config.xml檔案,然後通過SqlSessionFactoryBuilder.build()
方法,建立了一個SqlSessionFactory(這裡用到了工廠模式和構建者模式),前面說過,MyBatis就是通過我們寫的xml配置檔案,來構建配置物件的,那麼配置檔案所在的地方,就一定是構建開始的地方,也就是build方法。
進入build方法
建立XMLConfigBuilder解析配置資訊,之後呼叫parser()解析返回Configuration物件,呼叫build方法來建立SqlSessionFactoryfactory
介面的預設實現DefaultSqlSessionFactory
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//建立出 XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//解析xml檔案分析
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//build()方法--構建者模式建立工廠物件
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
XMLConfigBuilder類
建立XmlConfigbuilder物件,mybaitis中的構建起都繼承了一個BaseBuilder的類,該類維護了三個變數
- Configuration 配置類–主配置
- TypeAliasRegistry 型別別名註冊器
- TypeHandlerRegistry 型別處理器註冊器(用於型別轉換)
configuration實際上就是一個維護了mybatis配置資訊和執行相關的執行器和處理器的配置類(配置資訊對映關係)
//上面6個建構函式最後都合流到這個函式,傳入XPathParser
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//首先呼叫父類初始化Configuration
super(new Configuration());
//錯誤上下文設定成SQL Mapper Configuration(XML檔案配置),以便後面出錯了報錯用吧
ErrorContext.instance().resource("SQL Mapper Configuration");
//將Properties全部設定到Configuration裡面去
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
構建該類初始化了Configuration,用於後面解析配置類解析完注入,特別是多個繼承了BaseBuilder的構建器都用來操作該Configuration
初始化完,呼叫parse()方法
//解析配置
public Configuration parse() {
//如果已經解析過了,報錯
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// <?xml version="1.0" encoding="UTF-8" ?>
// <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
// "http://mybatis.org/dtd/mybatis-3-config.dtd">
// <configuration>
// <environments default="development">
// <environment id="development">
// <transactionManager type="JDBC"/>
// <dataSource type="POOLED">
// <property name="driver" value="${driver}"/>
// <property name="url" value="${url}"/>
// <property name="username" value="${username}"/>
// <property name="password" value="${password}"/>
// </dataSource>
// </environment>
// </environments>
// <mappers>
// <mapper resource="org/mybatis/example/BlogMapper.xml"/>
// </mappers>
// </configuration>
//根節點是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
該配置解析了主配置類中的configuration節點
xml解析使用了java提供的xml解析工具,mybatis進一步封裝了相關的解析方法,原理就是解析xml語法樹
初始化生成Configuration流程圖
進一步檢視parser.evalNode("/configuration")
該方法分步驟解析根節點下的配置資訊(使用 Xpath解析器來解析)
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步驟解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.型別別名
typeAliasesElement(root.evalNode("typeAliases"));
//3.外掛
pluginElement(root.evalNode("plugins"));
//4.物件工廠
objectFactoryElement(root.evalNode("objectFactory"));
//5.物件包裝工廠
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.設定
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.環境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.型別處理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.對映器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
解析對映器Mappers
其他部分程式碼不是主要了解內容,我們主要了解mapper對映器以及對映器對應xxxMapper.xml檔案時是如何被解析的
跟蹤對映器部分程式碼
mapperElement(root.evalNode(“mappers”));
首先我們先了解對映器的幾種配置,檢視官方文件可知有如下配置:
<!-- 使用相對於類路徑的資源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定資源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用對映器介面實現類的完全限定類名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 將包內的對映器介面實現全部註冊為對映器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
mapperElement()方法中也分別解析了下面幾種配置方法
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自動掃描包下所有對映器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//10.1使用類路徑
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//對映器比較複雜,呼叫XMLMapperBuilder
//注意在for迴圈裡每個mapper都重新new一個XMLMapperBuilder,來解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用絕對url路徑
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//對映器比較複雜,呼叫XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java類名
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.");
}
}
}
}
}
分析上面程式碼可以看出:
- 對映器配置 使用類介面的-----直接呼叫addMapper()方法
- 對映器配置 使用xxxMapper.xml則呼叫XmlMapper來解析
分別分析這兩種方式的
Mapper配置使用類介面
跟蹤原始碼
MapperRegistry
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
//查詢包下所有是superType的類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
//看一下如何新增一個對映
public <T> void addMapper(Class<T> type) {
//mapper必須是介面!才會新增
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重複新增了,報錯
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
//在執行解析器之前新增型別很重要
//否則,對映器解析器可能會自動嘗試繫結。 如果型別是已知的,則不會嘗試。
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果載入過程中出現異常需要再將這個mapper從mybatis中刪除,這種方式比較醜陋吧,難道是不得已而為之?
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
從這裡發現對映器的關係通過MapperRegistry序號產生器來維護,該類維護了配置類和對映關係map,
public class MapperRegistry {
private Configuration config;
//將已經新增的對映都放入HashMap
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public MapperRegistry(Configuration config) {
this.config = config;
}
...
}
將類加入序號產生器後,使用MapperAnnotationBuilder 來解析parse()
檢視該方法
public void parse() {
//type即介面類,resource為該類全路徑
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
//解析對應xxxMapper.xml,並標識為已載入了
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
//快取
parseCache();
parseCacheRef();
//解析類上的介面
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 如果不是橋接方法
if (!method.isBridge()) {
//解析method中的sql註解
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
檢視loadXmlResource
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//查詢同包下的xml檔案,並解析
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
//構建xml解析工具來解析
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
程式碼分析到這裡,我們可以看出大概的流程:
- 將該類介面注入
MapperRegistry
中,通過Map<Class<?>, MapperProxyFactory<?>> knownMappers
集合來維護,把類介面和新生成的MapperProxyFactory代理工廠作為鍵值對 - 解析該介面類下的xml檔案
- 解析類介面上的註解
Mapper配置使用xml檔案
xml檔案的方式首先通過XMLMapperBuilder解析xml檔案【該類也在介面方式中呼叫】
這裡跟蹤下XMLMapperBuilder是如何解析資訊的
public void parse() {
//判斷檔案是否之前解析過
if (!configuration.isResourceLoaded(resource)) {
//解析mapper檔案節點(主要)(下面貼了程式碼)
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//繫結Namespace裡面的Class物件---實際上就是新增到MapperRegistry中
bindMapperForNamespace();
}
//重新解析之前解析不了的節點
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
//解析mapper檔案裡面的節點
// 拿到裡面配置的配置項 最終封裝成一個MapperedStatemanet
private void configurationElement(XNode context) {
try {
//獲取名稱空間 namespace,這個很重要,後期mybatis會通過這個動態代理我們的Mapper介面
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
//如果namespace為空則拋一個異常
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//解析快取節點
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//解析parameterMap(過時)和resultMap <resultMap></resultMap>
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析<sql>節點
//<sql id="staticSql">select * from test</sql> (可重用的程式碼段)
//<select> <include refid="staticSql"></select>
sqlElement(context.evalNodes("/mapper/sql"));
//解析增刪改查節點<select> <insert> <update> <delete>
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);
}
}
在這個parse()方法中,呼叫了一個configuationElement程式碼,用於解析XXXMapper.xml檔案中的各種節點,包括<cache>
、<cache-ref>
、<paramaterMap>
(已過時)、<resultMap>
、<sql>
、還有增刪改查節點,和上面相同的是,我們也挑一個主要的來說,因為解析過程都大同小異。
同時將xml檔案上namespace對應的介面註冊到MappperRegistry序號產生器中。
這裡我們介紹下增刪改查節點的方法——buildStatementFromContext(),和JDBC一樣該Statement就是運算元據庫的物件
//7.配置select|insert|update|delete
private void buildStatementFromContext(List<XNode> list) {
//呼叫7.1構建語句
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
//7.1構建語句
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//構建所有語句,一個mapper下可以有很多select
//語句比較複雜,核心都在這裡面,所以呼叫XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//核心XMLStatementBuilder.parseStatementNode--這部分和類介面中解析sql註解的邏輯一樣
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//如果出現SQL語句不完整,把它記下來,塞到configuration去
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
//獲取<select id="xxx">中的id
String id = context.getStringAttribute("id");
//獲取databaseId 用於多資料庫,這裡為null
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//獲取節點名 select update delete insert
String nodeName = context.getNode().getNodeName();
//根據節點名,得到SQL操作的型別
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);
//替換Includes標籤為對應的sql標籤裡面的值
includeParser.applyIncludes(context.getNode());
//獲取parameterType名
String parameterType = context.getStringAttribute("parameterType");
//獲取parameterType的Class
Class<?> parameterTypeClass = resolveClass(parameterType);
//解析配置的自定義指令碼語言驅動 這裡為null
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
//解析selectKey
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//設定主鍵自增規則
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
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語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//獲取StatementType,可以理解為Statement和PreparedStatement
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
Class<?> resultTypeClass = resolveClass(resultType);
//獲取resultMap的id
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");
//將剛才獲取到的屬性,封裝成MappedStatement物件(程式碼貼在下面)
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
//將剛才獲取到的屬性,封裝成MappedStatement物件
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
//id = namespace
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//通過構造者模式+鏈式變成,構造一個MappedStatement的構造者
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
MappedStatement statement = statementBuilder.build();
//將MappedStatement物件封裝到Configuration物件中
configuration.addMappedStatement(statement);
return statement;
}
將xml中的節點解析,並封裝一個MappedStatement物件,並新增在Configuration中和Map集合中
構建過程流程圖:
SQL語句解析
在剛才過程中包含了SQL語句的生成,在這裡進一步分析
//解析Sql(重要) 根據sql文字來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
這裡就是生成Sql的入口,以單步除錯的角度接著往下看。
/*進入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//進入這個構造
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//進入parseScriptNode
return builder.parseScriptNode();
}
/**
進入這個方法
*/
public SqlSource parseScriptNode() {
//#
//會先解析一遍
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//如果是${}會直接不解析,等待執行的時候直接賦值
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//用佔位符方式來解析 #{} --> ?
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//獲取select標籤下的子標籤
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
//如果是查詢
//獲取原生SQL語句 這裡是 select * from test where id = #{id}
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//檢查sql是否是${}
if (textSqlNode.isDynamic()) {
//如果是${}那麼直接不解析
contents.add(textSqlNode);
isDynamic = true;
} else {
//如果不是,則直接生成靜態SQL
//#{} -> ?
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);
}
/*從上面的程式碼段到這一段中間需要經過很多程式碼,就不一段一段貼了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//這裡會生成一個GenericTokenParser,傳入#{}作為開始和結束,然後呼叫其parse方法,即可將#{}換為 ?
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//這裡可以解析#{} 將其替換為?
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
//經過一段複雜的解析過程
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
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;
//遍歷裡面所有的#{} select ? ,#{id1} ${}
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 {
//使用佔位符 ?
//注意handler.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();
}
//BindingTokenParser 的handleToken
//當掃描到${}的時候呼叫此方法 其實就是不解析 在執行時候在替換成具體的值
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
//ParameterMappingTokenHandler的handleToken
//全域性掃描#{id} 字串之後 會把裡面所有 #{} 呼叫handleToken 替換為?
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
首先這裡會通過<select>
節點獲取到我們的SQL語句,假設SQL語句中只有${}
,那麼直接就什麼都不做,在執行的時候直接進行賦值。
而如果掃描到了#{}
字串之後,會進行替換,將#{}
替換為 ?
。
那麼他是怎麼進行判斷的呢?
這裡會生成一個GenericTokenParser,這個物件可以傳入一個openToken和closeToken,如果是#{}
,那麼openToken就是#{
,closeToken就是 }
,然後通過parse方法中的handler.handleToken()
方法進行替換。
在這之前由於已經進行過SQL是否含有#{}
的判斷了,所以在這裡如果是隻有${}
,那麼handler就是BindingTokenParser的例項化物件,如果存在#{}
,那麼handler就是ParameterMappingTokenHandler的例項化物件。
分別進行處理。
小結
至此整個MyBatis的查詢前構建的過程就基本說完了,簡單地總結就是,MyBatis會在執行查詢之前,對配置檔案進行解析成配置物件:Configuration,以便在後面執行的時候去使用,而存放SQL的xml又會解析成MappedStatement物件,但是最終這個物件也會加入Configuration中,將Configuration物件通過build()方法來建立工廠物件
相關文章
- vue原始碼解讀-建構函式Vue原始碼函式
- redux原始碼解讀(簡單易懂版)Redux原始碼
- Retrofit原始碼解讀(一)--Retrofit簡單流程原始碼
- LinkedHashMap,原始碼解讀就是這麼簡單HashMap原始碼
- Mybatis原始碼分析(一)Mybatis的架構設計簡介MyBatis原始碼架構
- Vite 原始碼解讀系列(圖文結合) —— 構建篇Vite原始碼
- Mybatis 原始碼解讀-設計模式總結MyBatis原始碼設計模式
- mybatis原始碼解讀---一條sql的旅程MyBatis原始碼SQL
- httprunner3原始碼解讀(1)簡單介紹原始碼模組內容HTTP原始碼
- 簡單易懂的Vue資料繫結原始碼解讀Vue原始碼
- Flutter 極簡 App 程式碼簡單解讀FlutterAPP
- 短視訊平臺原始碼,構建簡單的底部導航欄原始碼
- PostgreSQL 原始碼解讀(240)- HTAB簡介SQL原始碼
- PostgreSQL 原始碼解讀(249)- 實現簡單的鉤子函式SQL原始碼函式
- 閱讀mybatis的原始碼的思路MyBatis原始碼
- mybatis原始碼-註解sqlMyBatis原始碼SQL
- Laravel 原始碼閱讀指南 -- Database 查詢構建器Laravel原始碼Database
- Langchain-ChatGLM原始碼解讀(二)-文件embedding以及構建faiss過程LangChain原始碼
- 精盡MyBatis原始碼分析 - 文章導讀MyBatis原始碼
- MyBatis原始碼窺探(一):MyBatis整體架構解析MyBatis原始碼架構
- Magic原始碼閱讀(三)——資料匯入和構建原始碼
- PostgreSQL 原始碼解讀(216)- 實現簡單的擴充套件函式SQL原始碼套件函式
- Vue2原始碼解讀(4) - 響應式原理及簡單實現Vue原始碼
- 簡單讀讀原始碼 - dubbo多提供者(provider)配置方法原始碼IDE
- 構建最簡單陣列陣列
- 程式碼來構建一個簡單的compilerCompile
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- Mybatis原始碼如何閱讀,教你一招!!!MyBatis原始碼
- 閱讀vue原始碼後,簡單實現虛擬domVue原始碼
- WeakHashMap,原始碼解讀HashMap原始碼
- Handler原始碼解讀原始碼
- Laravel 原始碼解讀Laravel原始碼
- Swoft 原始碼解讀原始碼
- SDWebImage原始碼解讀Web原始碼
- MJExtension原始碼解讀原始碼
- Masonry原始碼解讀原始碼
- HashMap原始碼解讀HashMap原始碼
- Redux原始碼解讀Redux原始碼