Mybatis如何載入配置檔案 原始碼解讀parameterType
我能學到什麼
--------------------------------------------------------------------------------------------------------------------------------------------------1. Mybatis載入解析配置檔案流程
2. 如何解析配置檔案裡面的parameterType
3. 提高看原始碼的能力
4. 檢視原始碼編寫方式,明白應該如何規範的寫出解析XML檔案的程式碼,提高編碼能力
5. 學會使用框架上解決問題的思維和常用手段
- l 利用配置檔案解耦
- l 利用反射、動態代理、泛型、設計模式等解決框架級別的問題。
- l 框架的設計思想
----------------------------------------------------------------------------------------------------------------------------------------------------
題外問題
如下一段程式碼:呼叫了Mybatis提供的載入及解析配置檔案功能。
public class DBAccess { public SqlSession getSqlSession() throws IOException { //1、通過配置檔案獲取資料庫連線相關資訊 Readerreader=Resources.getResourceAsReader("hdu/terence/config/Configuration.xml"); //2、通過配置資訊構建SqlSessionFactory SqlSessionFactorySSF=new SqlSessionFactoryBuilder().build(reader); //3、通過SqlSessionFactory開啟資料庫會話 SqlSessionsqlSession=SSF.openSession(); return sqlSession; } }
題外話
上述程式碼在Mybatis中的使用存在兩個問題:
問題一:每次訪問資料呼叫Sql語句的時候,都會臨時的去呼叫載入配置檔案解析,很耗效能。
問題二:另外,每次訪問,都需要反覆載入,耗費時間。
這個問題的暫且說一下解決辦法:
針對第一個配置檔案載入的時機問題,要自己寫一個監聽器,容器在啟動的時候載入配置檔案。
【載入時機】-->【監聽器】
針對第二個問題,通過單利模式存放監聽器載入的配置內容,防止其重複載入。
【重複載入】-->【單例模式】
在實際開發中則是通過Spring+Mybatis解決上述兩個問題的。
原始碼解讀
載入上篇
先說一下上述載入解析配置檔案的程式碼:根據註釋可知,此部分分為三步。
1. 通過配置檔案獲取資料庫連線相關資訊
2. 通過配置資訊構建SqlSessionFactory
3. 通過SqlSessionFactory開啟資料庫會話
其中,在第二步通過build()方法進入Mybatis當中。
上述build()方法是在SqlSessionFactoryBuilder.class這個類中實現的:
public SqlSessionFactorybuild(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previouserror. } } }
看此句:
XMLConfigBuilderparser = new XMLConfigBuilder(reader, environment, properties)表示將轉換後的reader、配置環境以及配置的各個屬性包裝在parser解析項中,然後通過return build(parser.parse())返回一個會話工廠,仍然需要進入另外的原始碼XMLConfigBilder.class中,找到parser()解析方法:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only beused once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
在建構函式當中,將parsed=flase,在configuration parse()方法中,先判別parsed是否為ture,如果是True,則表示已經載入解析過,丟擲異常(防止重複載入,耗費效能耗費時間,防止出現類似於呼叫sql語句時候每次都呼叫DBAccesss一樣出現的兩個問題),否則將其賦值為true,然後繼續解析載入檔案,通過parser.evalNode("/configuration")進入XPathParser.Class中,通過裡面的Document檔案讀取物件來解析檔案(那麼由此可以說明,Mybatis解析xml檔案使用的是Dom物件和Java JDK中的類進行的)。
XPathParser.Class檔案相關內容:
建構函式:
public XPathParser(String xml) { commonConstructor(false, null, null); this.document = createDocument(new InputSource(new StringReader(xml))); } public XPathParser(Reader reader) { commonConstructor(false, null, null); this.document = createDocument(new InputSource(reader)); }
看第二個建構函式可知此步表示reader物件的轉化為輸入流,然後轉化為document物件。
配置檔案
上述的原始碼目的是為了進入配置檔案Configuration.xml檔案解析,下面先貼出來配置檔案
貼總配置檔案Configuration.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> <!-- 配置宣告攔截器,可在攔截器中獲取該配置中的屬性值,用作其他用途 --> <plugins> <plugin interceptor="hdu.terence.interceptor.PageInterceptor"> <property name="test"value="123"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"> <property name="" value=""/> </transactionManager> <dataSource type="UNPOOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <!--url連線,注意編碼方式的指定--> <property name="url" value="jdbc:mysql://127.0.0.1:3306/micromessage?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <!--對映配置檔案,多個配置檔案可以寫多個mapper對映--> <mappers> <mapper resource="hdu/terence/config/sqlxml/Message.xml"/> <mapper resource="hdu/terence/config/sqlxml/Command.xml"/> <mapper resource="hdu/terence/config/sqlxml/CommandContent.xml"/> </mappers> </configuration>
子配置檔案Dao.xml---- Message.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="hdu.terence.dao.IMessage"> <resultMap type="hdu.terence.bean.Message" id="MessageResult"> <!--存放Dao值--> <!--type是和資料庫對應的bean類名Message--> <id column="id" jdbcType="INTEGER"property="id"/> <!--主鍵標籤--> <result column="COMMAND" jdbcType="VARCHAR"property="command"/> <result column="DESCRIPTION" jdbcType="VARCHAR" property="description"/> <result column="CONTENT" jdbcType="VARCHAR"property="content"/> </resultMap> <select id="queryMessageList" parameterType="java.util.Map" resultMap="MessageResult"> select <include refid="columns"/> from MESSAGE <where> <if test="message.command != null and!"".equals(message.command.trim())"> andCOMMAND=#{message.command} </if> <if test="message.description != null and!"".equals(message.description.trim())"> andDESCRIPTION like '%' #{message.description} '%' </if> </where> order by ID limit#{page.dbIndex},#{page.dbNumber} </select> </mapper>
梳理總流程
OK,退出來梳理總流程:
首先,建立一個總配置檔案的解析流,使用Document物件代替reader物件,成為了新的配置檔案解析代言人,然後Document物件進入Configuration.xml配置檔案,解析出<mappers>……</mappers>的配置項,找到Dao.xml配置檔案的路徑,根據該路徑,進入該配置檔案(Message.xml)。
最後,進入Dao.xml配置檔案之後,使用JDK中的Dom物件逐個解析,肯定能找到<select>標籤,然後找到該標籤後面的屬性parameterTypes引數型別屬性,最後通過反射機制,利用引數型別獲取引數類名。
載入下篇
下面就解讀一下如何查詢總配置檔案和在總配置檔案進入到sql語句Dao.xml配置檔案:
先說一個東西:XPath
Xpath是XML的路徑語言,使用路徑表示式來表示配置文件中的結點、結點集:
XMLConfigBuilder.class: public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only beused once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
這個方法就是利用Xpath的XML路徑語言(也是一種路徑表示式),獲取的是總配置檔案Configuration.xml檔案的<configuration>這個根結點:
進入到XPathParser.class方法:
public XNode evalNode(String expression) { return evalNode(document, expression); //document是解讀物件,expresssion是路徑表示式 } public XNode evalNode(Object root, String expression) { Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } return new XNode(this, node, variables); } public XNode evalNode(Object root, String expression) { Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } return new XNode(this, node, variables); } private Object evaluate(String expression, Object root, QName returnType) { try { return xpath.evaluate(expression, root, returnType); //在此步就給出了要取出的引數的型別。 } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } }
主線裡面有輔線,一層一層如同遞迴一樣呼叫,將獲取的物件儲存下來,然後使用XMLConfigBuilder.class檔案下的parseConfigation(XNode root)方法來解析內容。
private void parseConfiguration(XNode root) { try { Properties settings = settingsAsPropertiess(root.evalNode("settings")); //issue #117 read properties first propertiesElement(root.evalNode("properties")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory andobjectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL MapperConfiguration. Cause: " + e, e); } }
此方法同樣是利用路徑表示式(上述程式碼中括號中引號裡面的內容)遍歷root根節點下層的結點,比如在總配置檔案configuration.xml檔案plugins用來配置外掛或攔截器,environments用來配置資料庫訪問的各個屬性,mappers用來配置sql語句檔案dao.xml的對映路徑。
同樣的,evalNode()方法就是上述用到的查詢根節點的方法。
看分支語句:mapperElement(root.evalNode("mappers"))通過呼叫mapperElement()入sql配置檔案Dao.xml解析檔案。
看mapperElement()方法
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { 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) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuildermapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuildermapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify aurl, resource or class, but not more than one."); } } } } }
該方法首先判斷父節點是否為空,若不為空,則進入子節點<mapper>:
如果存在包檔案”package”,則進入configuration.addMappers(mapperPackage)分支;
否則利用getStringAttribute()方法獲取所有的屬性名稱和屬性值;
若僅Resource不為空,則將該節點暫存於執行緒池: (ErrorContext.instance().resource(resource))
然後利用Resouces.getResourceAsStream(resource)位元組流方法獲取該節點的屬性值: resource=”hdu/terence/config/sqlxml/Message.xml”
將引數儲存到流中: XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,configuration, url,configuration.getSqlFragments());
然後呼叫mapperParser.parse()方法解析,此方法需要進入XMLMapperBuilder.java類中
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingChacheRefs(); parsePendingStatements(); }
過程和上述判斷類似:首先判斷是否對其解析過,如果解析過,丟擲異常,否則繼續解析檔案。
在parser()方法中利用parser.evalNode("/mapper")找到根節點<mapper>,然後利用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 beempty");
}
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"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause:" + e, e);
}
}
由上述程式碼可知,必須要配置namespace屬性值,否則會丟擲異常:Mapper's namespace cannot beempty。
然後依次獲取parameterMap/resultMap/sql等內容,然後呼叫buildStatementFromContext(context.evalNodes("select|insert|update|delete")),對這些不同型別的sql語句進行處理,追溯--------------到如下函式(中間曲折迂迴太多了)。
public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } 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); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); 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); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing XMLIncludeTransformerincludeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes andremove them. 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"); 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)) ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); }
原始碼主要通過語句:
Class<?> parameterTypeClass= resolveClass(parameterType);
根據引數型別獲取引數類名。
獲取引數類名又是一個艱辛的過程: 通過函式resolveClass(parameterType)來一層一層追溯目標到 TypeAliasRegistry下的resolveAlias(alias)方法
TypeAliaiRegistry類:中成員和方法
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>(); public TypeAliasRegistry() { registerAlias("string", String.class); registerAlias("byte", Byte.class); registerAlias("long", Long.class); registerAlias("short", Short.class); registerAlias("int", Integer.class); registerAlias("integer", Integer.class); registerAlias("double", Double.class); registerAlias("float", Float.class); registerAlias("boolean", Boolean.class); registerAlias("byte[]", Byte[].class); registerAlias("long[]", Long[].class); registerAlias("short[]", Short[].class); registerAlias("int[]", Integer[].class); registerAlias("integer[]", Integer[].class); registerAlias("double[]", Double[].class); registerAlias("float[]", Float[].class); registerAlias("boolean[]", Boolean[].class); registerAlias("_byte", byte.class); registerAlias("_long", long.class); registerAlias("_short", short.class); registerAlias("_int", int.class); registerAlias("_integer", int.class); registerAlias("_double", double.class); registerAlias("_float", float.class); registerAlias("_boolean", boolean.class); registerAlias("_byte[]", byte[].class); registerAlias("_long[]", long[].class); registerAlias("_short[]", short[].class); registerAlias("_int[]", int[].class); registerAlias("_integer[]", int[].class); registerAlias("_double[]", double[].class); registerAlias("_float[]", float[].class); registerAlias("_boolean[]", boolean[].class); registerAlias("date", Date.class); registerAlias("decimal", BigDecimal.class); registerAlias("bigdecimal", BigDecimal.class); registerAlias("biginteger", BigInteger.class); registerAlias("object", Object.class); registerAlias("date[]", Date[].class); registerAlias("decimal[]", BigDecimal[].class); registerAlias("bigdecimal[]", BigDecimal[].class); registerAlias("biginteger[]", BigInteger[].class); registerAlias("object[]", Object[].class); registerAlias("map", Map.class); registerAlias("hashmap", HashMap.class); registerAlias("list", List.class); registerAlias("arraylist", ArrayList.class); registerAlias("collection", Collection.class); registerAlias("iterator", Iterator.class); registerAlias("ResultSet", ResultSet.class); } public <T> Class<T> resolveAlias(String string) { try { if (string == null) { return null; } // issue #748 String key = string.toLowerCase(Locale.ENGLISH); Class<T> value; //存放類名 if (TYPE_ALIASES.containsKey(key)) { value = (Class<T>) TYPE_ALIASES.get(key); } else { value = (Class<T>) Resources.classForName(string); } return value; } catch (ClassNotFoundException e) { throw new TypeException("Could not resolve type alias '" + string + "'. Cause:" + e, e); } }
如果引數型別string!=null,則先將引數型別字串轉換為小寫key,然後判斷Type_Aliases這個Map物件裡面是否包含key,如果有,則獲取對應的類名(此處獲取的是java自定義好的類名,如String.class、Integer.class、Byte[].class、Collection.class等等),否則,直接利用反射獲取類名(此處獲取的類名是自定義的類名),然後將獲得類名返回。
其餘方法不再細說,可自行追溯。
再梳理一遍
上述這條解讀原始碼的主線是解析引數parameterType對應的類名:
reader(總配置檔案路徑)—>變為configuration代言人—>解讀Configuration.xml總配置檔案—>
找到<mappers>結點下的sqi子配置檔案的路徑—>進入子配置檔案—>利用JDK中Dom物件查詢節點—>
找到<select>節點—>獲取該節點的parameterType屬性—>根據該屬性值利用反射獲取對應類名。
OK,完畢!