mybatis源码学习(三):MappedStatement的解析过程
我們之前介紹過MappedStatement表示的是XML中的一個SQL。類當中的很多字段都是SQL中對應(yīng)的屬性。我們先來了解一下這個類的屬性:
public final class MappedStatement {private String resource;private Configuration configuration;//sql的IDprivate String id;//嘗試影響驅(qū)動程序每次批量返回的結(jié)果行數(shù)和這個設(shè)置值相等private Integer fetchSize;//SQL超時時間private Integer timeout;//Statement的類型,STATEMENT/PREPARE/CALLABLEprivate StatementType statementType;//結(jié)果集類型,FORWARD_ONLY/SCROLL_SENSITIVE/SCROLL_INSENSITIVE private ResultSetType resultSetType;//表示解析出來的SQLprivate SqlSource sqlSource;//緩存private Cache cache;//已廢棄private ParameterMap parameterMap;//對應(yīng)的ResultMapprivate List<ResultMap> resultMaps;private boolean flushCacheRequired;private boolean useCache;private boolean resultOrdered;//SQL類型,INSERT/SELECT/DELETEprivate SqlCommandType sqlCommandType;//和SELECTKEY標簽有關(guān)private KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;private boolean hasNestedResultMaps;//數(shù)據(jù)庫ID,用來區(qū)分不同環(huán)境private String databaseId;private Log statementLog;private LanguageDriver lang;//多結(jié)果集時private String[] resultSets;MappedStatement() {// constructor disabled }...}對一些重要的字段我都增加了備注,方便理解。其中真正表示SQL的字段是SqlSource這個對象。
SqlSource接口很簡單,只有一個getBound方法:
public interface SqlSource {BoundSql getBoundSql(Object parameterObject);}它有很多實現(xiàn),需要我們重點關(guān)注的是StaticSqlSource,RawSqlSource和DynamicSqlSource。在正式學(xué)習(xí)他們前,我們先了解一下Mybatis動態(tài)SQL和靜態(tài)SQL的區(qū)別。
動態(tài)SQL表示這個SQL節(jié)點中含有${}或是其他動態(tài)的標簽(比如,if,trim,foreach,choose,bind節(jié)點等),需要在運行時根據(jù)傳入的條件才能確定SQL,因此對于動態(tài)SQL的MappedStatement的解析過程應(yīng)該是在運行時。
而靜態(tài)SQL是不含以上這個節(jié)點的SQL,能直接解析得到含有占位符形式的SQL語句,而不需要根據(jù)傳入的條件確定SQL,因此可以在加載時就完成解析。所在在執(zhí)行效率上要高于動態(tài)SQL。
而DynamicSqlSource和RawSqlSource就分別對應(yīng)了動態(tài)SQL和靜態(tài)SQL,它們都封裝了StaticSqlSource。
我們先從簡單的入手,了解靜態(tài)SQL的解析過程。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper"><select id="selectAllAuthors" resultType="org.apache.ibatis.domain.blog.Author">select * from author</select></mapper>這是我們要解析的XML文件,mapper節(jié)點下只有一個select節(jié)點。
public class XmlMapperBuilderTest {@Testpublic void shouldSuccessfullyLoadXMLMapperFile() throws Exception {Configuration configuration = new Configuration();String resource = "org/apache/ibatis/builder/AuthorMapper.xml";InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());builder.parse();inputStream.close();} }這是我們測試解析過程的代碼。我們可以看到解析是由XMLMapperBuilder開始的。我們先了解一下它的字段:
public class XMLMapperBuilder extends BaseBuilder {//用來解析XMLprivate final XPathParser parser;//再解析完成后,用解析所得的屬性來幫助創(chuàng)建各個對象private final MapperBuilderAssistant builderAssistant;//保存SQL節(jié)點private final Map<String, XNode> sqlFragments;//... }它還從父類中繼承了configuration(配置對象),typeAliasRegistry(類型別名注冊器)和typeHandlerRegistry(類型處理器注冊器)。
接下來看一下它的parse方法:
public void parse() {//判斷是否已經(jīng)加載過資源 if (!configuration.isResourceLoaded(resource)) {//從mapper根節(jié)點開始解析configurationElement(parser.evalNode("/mapper"));//將該資源添加到為已經(jīng)加載過的緩存中 configuration.addLoadedResource(resource);//將解析的SQL和接口中的方法綁定 bindMapperForNamespace();}//對一些未完成解析的節(jié)點再解析 parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}主要的解析過程在configurationElement中:
private void configurationElement(XNode context) {try {//解析mapper的namespace屬性,并設(shè)置String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);//解析<cache-ref>節(jié)點,它有一個namespace屬性,表示引用該命名空間下的緩存cacheRefElement(context.evalNode("cache-ref"));//解析<cache>節(jié)點,可以設(shè)置緩存類型和屬性,或是指定自定義的緩存cacheElement(context.evalNode("cache"));//已廢棄,不再使用 parameterMapElement(context.evalNodes("/mapper/parameterMap"));//解析resultMap節(jié)點resultMapElements(context.evalNodes("/mapper/resultMap"));//解析<SQL>節(jié)點,SQL節(jié)點可以使一些SQL片段被復(fù)用 sqlElement(context.evalNodes("/mapper/sql"));//解析SQL語句(select|insert|update|delete節(jié)點) 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);}}我們關(guān)注SQL語句的解析過程,上述buildStatementFromContext(List<XNode>)方法會增加dateBaseId的參數(shù),然后調(diào)用另一個重載方法:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {//遍歷XNode節(jié)點for (XNode context : list) {//為每個節(jié)點創(chuàng)建XMLStatementBuilder對象,final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {//解析Node statementParser.parseStatementNode();} catch (IncompleteElementException e) {//對不能完全解析的節(jié)點添加到incompleteStatement,在parsePendingStatements方法中再解析 configuration.addIncompleteStatement(statementParser);}}}先看看XMLStatementBuilder對象:
public class XMLStatementBuilder extends BaseBuilder {private final MapperBuilderAssistant builderAssistant;private final XNode context;private final String requiredDatabaseId;// ... } View Code含有的字段相對簡單,不再具體解釋。直接看parseStatementNode方法:
?
public void parseStatementNode() {//獲取idString id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");//驗證databaseId是否匹配if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");//已廢棄String parameterMap = context.getStringAttribute("parameterMap");//參數(shù)類型;將會傳入這條語句的參數(shù)類的完全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過 TypeHandler 推斷出具體傳入語句的參數(shù),默認值為 unset。String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);//結(jié)果類型;外部 resultMap 的命名引用。String resultMap = context.getStringAttribute("resultMap");//結(jié)果類型;表示從這條語句中返回的期望類型的類的完全限定名或別名。注意如果是集合情形,那應(yīng)該是集合可以包含的類型,而不能是集合本身。不能和resultMap同時使用。String resultType = context.getStringAttribute("resultType");String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);Class<?> resultTypeClass = resolveClass(resultType);//結(jié)果集類型;FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個,默認值為 unset (依賴驅(qū)動)。String resultSetType = context.getStringAttribute("resultSetType");//STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);String nodeName = context.getNode().getNodeName();//SQLCommand類型SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;//flushCache;在執(zhí)行語句時表示是否刷新緩存boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);//是否對該語句進行二級緩存;默認值:對 select 元素為 true。boolean useCache = context.getBooleanAttribute("useCache", isSelect);//根嵌套結(jié)果相關(guān)boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);//引入SQL片段// Include Fragments before parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// Parse selectKey after includes and remove them.//處理selectKey processSelectKeyNodes(id, parameterTypeClass, langDriver);// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// String resultSets = context.getStringAttribute("resultSets");String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");//設(shè)置主鍵自增的方式 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;}//通過buildAssistant將解析得到的參數(shù)設(shè)置構(gòu)造成MappedStatement對象 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);}將解析得到參數(shù)通過BuilderAssistant.addMappedStatement方法,解析得到MappedStatement對象。
?
上面已經(jīng)說過sqlsource表示的一個SQL語句,因此我們關(guān)注langDriver.createSqlSource這個方法。看XMLLanguageDriver這個實現(xiàn)。
@Overridepublic SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();}可以看到他將創(chuàng)建sqlsource的工作交給了XMLScrpitBuilder(又一個創(chuàng)建者模式的應(yīng)用)。來看parseScriptNode方法:
public SqlSource parseScriptNode() {//解析SQL語句節(jié)點,創(chuàng)建MixedSqlNode對象MixedSqlNode rootSqlNode = parseDynamicTags(context);SqlSource sqlSource = null;//根據(jù)是否是動態(tài)的語句,創(chuàng)建DynamicSqlSource或是RawSqlSource對象,并返回if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource;}MixedSqlNode是SqlNode的一個實現(xiàn),包含了各個子節(jié)點,用來遍歷輸出子節(jié)點。SqlNode還有很多不同的實現(xiàn),分別對應(yīng)不同的節(jié)點類型。對應(yīng)關(guān)系如下:
| SqlNode實現(xiàn) | 對應(yīng)SQL語句中的類型 |
| TextSqlNode | ${} |
| IfSqlNode | If節(jié)點 |
| TrimSqlNode/WhereSqlNode/SetSqlNode | Trim/Where/Set節(jié)點 |
| Foreach節(jié)點 | foreach標簽 |
| ChooseSqlNode節(jié)點 | choose/when/otherwhise節(jié)點 |
| ValDeclSqlNode節(jié)點 | bind節(jié)點 |
| StaticTextSqlNode | 不含上述節(jié)點 |
?
?
?
?
?
?
除了StaticTextSqlNode節(jié)點外,其余對應(yīng)的都是動態(tài)語句。
因此我們本文的關(guān)注點在StaticTextSqlNode。
讓我們對應(yīng)文初sql語句的解析來看一下parseDynamicTags方法,為了便于理解,我將在右邊注釋出每一步的結(jié)果
protected MixedSqlNode parseDynamicTags(XNode node) {// node是我們要解析的SQL語句: <select resultType="org.apache.ibatis.domain.blog.Author" id="selectAllAuthors">select * from author</select>List<SqlNode> contents = new ArrayList<SqlNode>();//獲取SQL下面的子節(jié)點NodeList children = node.getNode().getChildNodes();//這里的children只有一個節(jié)點;//遍歷子節(jié)點,解析成對應(yīng)的sqlNode類型,并添加到contents中for (int i = 0; i < children.getLength(); i++) {XNode child = node.newXNode(children.item(i));//第一個child節(jié)點就是SQL中的文本數(shù)據(jù):select * from author//如果是文本節(jié)點,則先解析成TextSqlNode對象if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {//獲取文本信息String data = child.getStringBody("");//data:select * from author//創(chuàng)建TextSqlNode對象TextSqlNode textSqlNode = new TextSqlNode(data);//判斷是否是動態(tài)Sql,其過程會調(diào)用GenericTokenParser判斷文本中是否含有"${"字符if (textSqlNode.isDynamic()) {//如果是動態(tài)SQL,則直接使用TextSqlNode類型,并將isDynamic標識置為true contents.add(textSqlNode);isDynamic = true;} else {//不是動態(tài)sql,則創(chuàng)建StaticTextSqlNode對象,表示靜態(tài)SQLcontents.add(new StaticTextSqlNode(data));}} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他類型的節(jié)點,由不同的節(jié)點處理器來對應(yīng)處理成本成不同的SqlNode類型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;}}//用contents構(gòu)建MixedSqlNode對象return new MixedSqlNode(contents);}上述過程中,我們主要關(guān)注靜態(tài)SQL的解析過程,對于動態(tài)SQL的解析將在之后介紹。
得到MixedSqlNode后,靜態(tài)的SQL會創(chuàng)建出RawSqlSource對象。
看一下RawSqlSource:
public class RawSqlSource implements SqlSource {//內(nèi)部封裝的sqlSource對象,getBoundSql方法會委托給這個對象private final SqlSource sqlSource;public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {this(configuration, getSql(configuration, rootSqlNode), parameterType);}public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {//創(chuàng)建sqlSourceBuilderSqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> clazz = parameterType == null ? Object.class : parameterType;//解析sql,創(chuàng)建StaticSqlSource對象sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());}//獲取sql語句private static String getSql(Configuration configuration, SqlNode rootSqlNode) {DynamicContext context = new DynamicContext(configuration, null);//這里的rootSqlNode就是之前得到的MixedSqlNode,它會遍歷內(nèi)部的SqlNode,逐個調(diào)用sqlNode的apply方法。StaticTextSqlNode會直接context.appendSql方法 rootSqlNode.apply(context);return context.getSql();}@Overridepublic BoundSql getBoundSql(Object parameterObject) {return sqlSource.getBoundSql(parameterObject);}}代碼相對簡單,主要的步驟就是(1)通過SqlNode獲得原始SQL語句;(2)創(chuàng)建SqlSourceBuilder對象,解析SQL語句,并創(chuàng)建StaticSqlSource對象;(3)將getBoundSql方法委托給內(nèi)部的staticSqlSource對象。
其中比較關(guān)鍵的一步是解析原始SQL語句,并創(chuàng)建StaticSqlSource對象。因此我們繼續(xù)看SqlSourceBuilder對象。
public class SqlSourceBuilder extends BaseBuilder {private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";public SqlSourceBuilder(Configuration configuration) {super(configuration);}public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {//創(chuàng)建TokenHandler,用來將原始Sql中的'#{}' 解析成'?'ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);//解析原始sqlString sql = parser.parse(originalSql);//創(chuàng)建出StaticSqlSource對象return new StaticSqlSource(configuration, sql, handler.getParameterMappings());}private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();private Class<?> parameterType;private MetaObject metaParameters;public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {super(configuration);this.parameterType = parameterType;this.metaParameters = configuration.newMetaObject(additionalParameters);}public List<ParameterMapping> getParameterMappings() {return parameterMappings;}@Overridepublic String handleToken(String content) {//解析'#{}'中的參數(shù),創(chuàng)建ParameterMapping對象 parameterMappings.add(buildParameterMapping(content));//將'#{}'替換成'?'return "?";}private ParameterMapping buildParameterMapping(String content) {Map<String, String> propertiesMap = parseParameterMapping(content);String property = propertiesMap.get("property");Class<?> propertyType;if (metaParameters.hasGetter(property)) { // issue #448 get type from additional paramspropertyType = metaParameters.getGetterType(property);} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {propertyType = parameterType;} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {propertyType = java.sql.ResultSet.class;} else if (property == null || Map.class.isAssignableFrom(parameterType)) {propertyType = Object.class;} else {MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());if (metaClass.hasGetter(property)) {propertyType = metaClass.getGetterType(property);} else {propertyType = Object.class;}}ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);Class<?> javaType = propertyType;String typeHandlerAlias = null;for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {String name = entry.getKey();String value = entry.getValue();if ("javaType".equals(name)) {javaType = resolveClass(value);builder.javaType(javaType);} else if ("jdbcType".equals(name)) {builder.jdbcType(resolveJdbcType(value));} else if ("mode".equals(name)) {builder.mode(resolveParameterMode(value));} else if ("numericScale".equals(name)) {builder.numericScale(Integer.valueOf(value));} else if ("resultMap".equals(name)) {builder.resultMapId(value);} else if ("typeHandler".equals(name)) {typeHandlerAlias = value;} else if ("jdbcTypeName".equals(name)) {builder.jdbcTypeName(value);} else if ("property".equals(name)) {// Do Nothing} else if ("expression".equals(name)) {throw new BuilderException("Expression based parameters are not supported yet");} else {throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties);}}if (typeHandlerAlias != null) {builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));}return builder.build();}private Map<String, String> parseParameterMapping(String content) {try {return new ParameterExpression(content);} catch (BuilderException ex) {throw ex;} catch (Exception ex) {throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);}}}}parse方法主要分為以下幾步:
(1)創(chuàng)建了ParameterMappingTokenHandler對象
(2)將ParameterMappingTokenHandler對象傳入GenericTokenParser的構(gòu)造函數(shù)中,創(chuàng)建GenericTokenParser對象
(3)通過GenericTokenParser對象解析原始SQL,這個過程中會將#{}替換成?,并將#{}中的參數(shù),解析形成ParamterMapping對象
(4)用得到的SQL和ParamterMapping對象創(chuàng)建StaticSqlSource對象。
?
解析完成后回到一開始的XMLMapperBuilder,它會在資源添加到已加載的列表中,并bindMapperForNamespace方法中為創(chuàng)建的MappedStatement添加命名空間。
轉(zhuǎn)載于:https://www.cnblogs.com/insaneXs/p/9083003.html
總結(jié)
以上是生活随笔為你收集整理的mybatis源码学习(三):MappedStatement的解析过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 练习题|网络编程-socket开发
- 下一篇: windows 提权 cve-2018-