config.xml解析為org.w3c.dom.Document
本文首先來簡單看一下MyBatis中將config.xml解析為org.w3c.dom.Document的流程,程式碼為上文的這部分:
1 static { 2 try { 3 reader = Resources.getResourceAsReader("mybatis/config.xml"); 4 ssf = new SqlSessionFactoryBuilder().build(reader); 5 } 6 catch (IOException e) { 7 e.printStackTrace(); 8 } 9 }
第3行的程式碼實現為:
1 public static Reader getResourceAsReader(String resource) throws IOException { 2 Reader reader; 3 if (charset == null) { 4 reader = new InputStreamReader(getResourceAsStream(resource)); 5 } else { 6 reader = new InputStreamReader(getResourceAsStream(resource), charset); 7 } 8 return reader; 9 }
相當於就是將輸入的路徑轉換為一個字元輸入流並返回。
接著繼續看靜態塊第4行的程式碼,new SqlSessionFactoryBuilder().build(reader),把程式碼定位到SqlSessionFactoryBuilder類的builder方法,這裡使用了多型,直接跟到build方法:
1 public SqlSessionFactory build(Reader reader, String environment, Properties properties) { 2 try { 3 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); 4 return build(parser.parse()); 5 } catch (Exception e) { 6 throw ExceptionFactory.wrapException("Error building SqlSession.", e); 7 } finally { 8 ErrorContext.instance().reset(); 9 try { 10 reader.close(); 11 } catch (IOException e) { 12 // Intentionally ignore. Prefer previous error. 13 } 14 } 15 }
解析config.xml的程式碼在第3行XMLConfigBuilder類的構造方法中,看一下XMLConfigBuilder類的構造方法做了什麼:
1 public XMLConfigBuilder(Reader reader, String environment, Properties props) { 2 this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); 3 }
這裡的關鍵是第二行程式碼的第一個引數XPathParser,看一下例項化XPathParser類的程式碼:
1 public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { 2 commonConstructor(validation, variables, entityResolver); 3 this.document = createDocument(new InputSource(reader)); 4 }
第2行的程式碼commonConstructor方法沒什麼好看的,將validation、variables、entityResolver設定到XPathParser類的引數中而已,順便再例項化一個javax.xml.xpath.XPath出來,XPath用於在XML文件中通過元素和屬性進行導航,並對元素和屬性進行遍歷。
接著看第3行的createDocument方法:
1 private Document createDocument(InputSource inputSource) { 2 // important: this must only be called AFTER common constructor 3 try { 4 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 5 factory.setValidating(validation); 6 7 factory.setNamespaceAware(false); 8 factory.setIgnoringComments(true); 9 factory.setIgnoringElementContentWhitespace(false); 10 factory.setCoalescing(false); 11 factory.setExpandEntityReferences(true); 12 13 DocumentBuilder builder = factory.newDocumentBuilder(); 14 builder.setEntityResolver(entityResolver); 15 builder.setErrorHandler(new ErrorHandler() { 16 @Override 17 public void error(SAXParseException exception) throws SAXException { 18 throw exception; 19 } 20 21 @Override 22 public void fatalError(SAXParseException exception) throws SAXException { 23 throw exception; 24 } 25 26 @Override 27 public void warning(SAXParseException exception) throws SAXException { 28 } 29 }); 30 return builder.parse(inputSource); 31 } catch (Exception e) { 32 throw new BuilderException("Error creating document instance. Cause: " + e, e); 33 } 34 }
看一下第5行~第11行的程式碼設定DocumentBuilderFactory中引數的含義:
- setValidating表示是否驗證xml檔案,這個驗證是DTD驗證
- setNamespaceAware表示是否支援xml名稱空間
- setIgnoringComments表示是否忽略註釋
- setIgnoringElementContentWhitespace表示是否忽略元素中的空白
- setCoalescing表示是否將CDATA節點轉換為Text節點,並將其附加到相鄰(如果有)的Text節點
- setExpandEntityReferences表示是否擴充套件實體引用節點
第13行的程式碼由設定的引數從DocumentBuilderFactory中獲取一個DocumentBuilder例項DocumentBuilderImpl,並由第14行的程式碼設定一個實體解析器,由第15行~第29行的程式碼設定一個錯誤處理器。
最後看一下第30行的程式碼parse方法:
1 public Document parse(InputSource is) throws SAXException, IOException { 2 if (is == null) { 3 throw new IllegalArgumentException( 4 DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, 5 "jaxp-null-input-source", null)); 6 } 7 if (fSchemaValidator != null) { 8 if (fSchemaValidationManager != null) { 9 fSchemaValidationManager.reset(); 10 fUnparsedEntityHandler.reset(); 11 } 12 resetSchemaValidator(); 13 } 14 domParser.parse(is); 15 Document doc = domParser.getDocument(); 16 domParser.dropDocumentReferences(); 17 return doc; 18 }
看過Spring配置檔案解析原始碼的朋友應該對這一段程式碼比較熟悉,一樣的,使用DocumentBuilder將解析InputSource成org.w3c.dom.Document並將Document儲存到XPathParser中。
Document轉換為Configuration
前面的程式碼將config.xml轉換為了org.w3c.dom.Document,下一步就是將org.w3c.dom.Document中的內容轉換為Java物件了,其中最主要的一個物件就是org.apache.ibatis.session.Configuration,還是回到之前的SqlSessionFactoryBuilder的build方法:
1 public SqlSessionFactory build(Reader reader, String environment, Properties properties) { 2 try { 3 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); 4 return build(parser.parse()); 5 } catch (Exception e) { 6 throw ExceptionFactory.wrapException("Error building SqlSession.", e); 7 } finally { 8 ErrorContext.instance().reset(); 9 try { 10 reader.close(); 11 } catch (IOException e) { 12 // Intentionally ignore. Prefer previous error. 13 } 14 } 15 }
先看一下第4行的parse方法,parse方法是XMLConfigBuilder中的,之前重點分析了它的屬性XPathParser,看一下XMLConfigBuilder的parse方法是如何實現的:
1 public Configuration parse() { 2 if (parsed) { 3 throw new BuilderException("Each XMLConfigBuilder can only be used once."); 4 } 5 parsed = true; 6 parseConfiguration(parser.evalNode("/configuration")); 7 return configuration; 8 }
這裡看一下第6行,可以使用XPathParser的evalNode方法解析標籤,後面解析標籤會大量用到此方法,此方法將標籤解析為XNode,像config.xml(可見上一篇文章的示例)解析完之後的XNode,toString()方法輸出的內容是這樣的:
<configuration> <properties resource="properties/db.properties"/> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="useGeneratedKeys" value="true"/> </settings> <typeAliases> <typeAlias alias="Mail" type="org.xrq.mybatis.pojo.Mail"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driveClass}"/> <property name="url" value="${url}"/> <property name="username" value="${userName}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mybatis/mail.xml"/> </mappers> </configuration>
可見xml檔案中<configuration>中所有內容都已經被成功解析並放在XNode中了,剩下的只要呼叫XNode的方法獲取自己想要的內容即可。
最後掃一眼parseConfiguration方法,之所以說掃一眼,因為之後要分析裡面的一些常用的和重點的內容,這裡只是列一下程式碼而已:
1 private void parseConfiguration(XNode root) { 2 try { 3 Properties settings = settingsAsPropertiess(root.evalNode("settings")); 4 //issue #117 read properties first 5 propertiesElement(root.evalNode("properties")); 6 loadCustomVfs(settings); 7 typeAliasesElement(root.evalNode("typeAliases")); 8 pluginElement(root.evalNode("plugins")); 9 objectFactoryElement(root.evalNode("objectFactory")); 10 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); 11 reflectorFactoryElement(root.evalNode("reflectorFactory")); 12 settingsElement(settings); 13 // read it after objectFactory and objectWrapperFactory issue #631 14 environmentsElement(root.evalNode("environments")); 15 databaseIdProviderElement(root.evalNode("databaseIdProvider")); 16 typeHandlerElement(root.evalNode("typeHandlers")); 17 mapperElement(root.evalNode("mappers")); 18 } catch (Exception e) { 19 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); 20 } 21 }
這裡就是逐個解析<configuration>標籤下的子標籤,並將資料設定到對應的屬性中,這裡要一個一個看一下。
settings解析
首先看settingsAsPropertiess(root.evalNode("settings"))這句程式碼,顯而易見這句話獲取了<configuration>下的<settings>節點。跟一下程式碼的實現:
1 private Properties settingsAsPropertiess(XNode context) { 2 if (context == null) { 3 return new Properties(); 4 } 5 Properties props = context.getChildrenAsProperties(); 6 // Check that all settings are known to the configuration class 7 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); 8 for (Object key : props.keySet()) { 9 if (!metaConfig.hasSetter(String.valueOf(key))) { 10 throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); 11 } 12 } 13 return props; 14 }
第5行將節點解析成鍵值對的形式(Properties是Hashtable的子類),看一下props的toString方法列印的內容:
{useGeneratedKeys=true, lazyLoadingEnabled=true, cacheEnabled=true}
可見settings裡面的資料已經被解析成了Properties了。之後還有一步,<settings>標籤下的每個<setting>中的name屬性不是隨便填寫的,都是MyBatis支援的配置,因此需要對Properties裡面的Key做一個校驗,校驗的程式碼就是第7行至第12行的程式碼,其中有一個name不是MyBatis支援的就會丟擲異常,MyBatis初始化整體失敗。
至於具體校驗的是哪些Key,這就要跟一下第7行的程式碼了,首先是MetaClass.forClass(Configuration.class, localReflectorFactory),第二個實參是XMLConfigBuilder裡面直接new出來的,它的實際型別為DefaultReflectorFactory,看一下forClass方法實現:
1 public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) { 2 return new MetaClass(type, reflectorFactory); 3 }
看一下new MetaClass做了什麼事:
1 private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) { 2 this.reflectorFactory = reflectorFactory; 3 this.reflector = reflectorFactory.findForClass(type); 4 }
顯而易見,繼續跟一下第3行的程式碼DefaultRelectorFactory的findForClass方法:
1 public Reflector findForClass(Class<?> type) { 2 if (classCacheEnabled) { 3 // synchronized (type) removed see issue #461 4 Reflector cached = reflectorMap.get(type); 5 if (cached == null) { 6 cached = new Reflector(type); 7 reflectorMap.put(type, cached); 8 } 9 return cached; 10 } else { 11 return new Reflector(type); 12 } 13 }
不管怎麼樣都會執行new Reflector(type)這一句程式碼,看一下此時做了什麼事,注意傳入的引數是Configuration的class物件:
1 public Reflector(Class<?> clazz) { 2 type = clazz; 3 addDefaultConstructor(clazz); 4 addGetMethods(clazz); 5 addSetMethods(clazz); 6 addFields(clazz); 7 readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); 8 writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); 9 for (String propName : readablePropertyNames) { 10 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); 11 } 12 for (String propName : writeablePropertyNames) { 13 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); 14 } 15 }
這麼多方法至於具體要看哪個,要注意的是之前XMLConfigBuilder裡面對於Key的判斷是"!metaConfig.hasSetter(String.valueOf(key))",程式碼的意思是判斷的是否Key有set方法,那麼顯而易見這裡要繼續跟第5行的addSetMethods方法:
1 private void addSetMethods(Class<?> cls) { 2 Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>(); 3 Method[] methods = getClassMethods(cls); 4 for (Method method : methods) { 5 String name = method.getName(); 6 if (name.startsWith("set") && name.length() > 3) { 7 if (method.getParameterTypes().length == 1) { 8 name = PropertyNamer.methodToProperty(name); 9 addMethodConflict(conflictingSetters, name, method); 10 } 11 } 12 } 13 resolveSetterConflicts(conflictingSetters); 14 }
到這裡應該很明顯了,結論就是:<setting>的name屬性對應的值,必須在Configuration類有相應的Setter,比如設定了一個屬性useGenerateKeys方法,那麼必須在Configuration類中有setUseGenerateKeys方法才行。
順便說一下第13行有一個resolveSetterConflicts方法,其作用是:Setter有可能在類中被過載導致有多個,此時取Setter中方法引數只有一個且引數型別與Getter一致的Setter。
properties解析
接著看一下propertiesElement(root.evalNode("properties"))方法,這句讀取的是<configuration>下的<properties>節點,程式碼實現為:
1 private void propertiesElement(XNode context) throws Exception { 2 if (context != null) { 3 Properties defaults = context.getChildrenAsProperties(); 4 String resource = context.getStringAttribute("resource"); 5 String url = context.getStringAttribute("url"); 6 if (resource != null && url != null) { 7 throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); 8 } 9 if (resource != null) { 10 defaults.putAll(Resources.getResourceAsProperties(resource)); 11 } else if (url != null) { 12 defaults.putAll(Resources.getUrlAsProperties(url)); 13 } 14 Properties vars = configuration.getVariables(); 15 if (vars != null) { 16 defaults.putAll(vars); 17 } 18 parser.setVariables(defaults); 19 configuration.setVariables(defaults); 20 } 21 }
看到第4行~第7行的程式碼指定了MyBatis的<properties>標籤下不能同時指定"resource"屬性和"url"屬性。
接著第9行~第13行的程式碼將.properties資源解析為Properties類,最後將Properties類設定到XPathParser和Configuration的variables屬性中,variables是一個Propreties變數。
型別別名解析
跳過loadCustomVfs(settings)直接看typeAliasesElement(root.evalNode("typeAliases"))這行,因為前者我也沒看懂幹什麼用的,後者是用於定義型別的別名的,解析的是<configuration>下的<typeAliases>標籤,用過MyBatis的應該很熟悉。看一下原始碼實現:
1 private void typeAliasesElement(XNode parent) { 2 if (parent != null) { 3 for (XNode child : parent.getChildren()) { 4 if ("package".equals(child.getName())) { 5 String typeAliasPackage = child.getStringAttribute("name"); 6 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); 7 } else { 8 String alias = child.getStringAttribute("alias"); 9 String type = child.getStringAttribute("type"); 10 try { 11 Class<?> clazz = Resources.classForName(type); 12 if (alias == null) { 13 typeAliasRegistry.registerAlias(clazz); 14 } else { 15 typeAliasRegistry.registerAlias(alias, clazz); 16 } 17 } catch (ClassNotFoundException e) { 18 throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); 19 } 20 } 21 } 22 } 23 }
從原始碼實現中我們可以知道兩點,<typeAliases>標籤下可以定義<package>和<typeAlias>兩種標籤,但是看第4行和第7行的判斷,這是一段if...else...,因此可以知道<package>標籤和<typeAlias>標籤只能定義其中的一種。首先看一下解析<package>標籤的程式碼,第6行的registerAliases方法:
1 public void registerAliases(String packageName, Class<?> superType){ 2 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); 3 resolverUtil.find(new ResolverUtil.IsA(superType), packageName); 4 Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); 5 for(Class<?> type : typeSet){ 6 // Ignore inner classes and interfaces (including package-info.java) 7 // Skip also inner classes. See issue #6 8 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { 9 registerAlias(type); 10 } 11 } 12 }
第3行根據路徑packageName尋找它下面的".class"檔案拿到所有的".class"檔案對應的類的Class,然後遍歷所有的Class,做了三層判斷
- 必須不是匿名類
- 必須不是介面
- 必須不是成員類
此時此Class對應的類符合條件,會進行註冊,通過registerAlias方法進行註冊,看一下方法實現:
1 public void registerAlias(Class<?> type) { 2 String alias = type.getSimpleName(); 3 Alias aliasAnnotation = type.getAnnotation(Alias.class); 4 if (aliasAnnotation != null) { 5 alias = aliasAnnotation.value(); 6 } 7 registerAlias(alias, type); 8 }
第2行獲取Class的simpleName,simpleName指的是移除了包名的名稱,比如aa.bb.cc.Mail,getSimpleName()獲取的就是Mail。
第3行獲取類上面的註解Alias,如果Alias註解中有定義value屬性且指定了值,那麼第4行~第6行的判斷優先取這個值作為Class的別名。
第7行註冊別名:
1 public void registerAlias(String alias, Class<?> value) { 2 if (alias == null) { 3 throw new TypeException("The parameter alias cannot be null"); 4 } 5 // issue #748 6 String key = alias.toLowerCase(Locale.ENGLISH); 7 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { 8 throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); 9 } 10 TYPE_ALIASES.put(key, value); 11 }
其實就做了兩步操作:
- 將alias全部小寫
- 將alias以及Class物件放到TYPE_ALIASES中,TYPE_ALIASES是一個HashMap
這樣一個流程,就將<package>標籤name屬性路徑下的Class(如果符合要求),全部放到了HashMap中以供使用。
接著看一下<typeAlias>標籤的解析,也就是前面說的else部分:
1 String alias = child.getStringAttribute("alias"); 2 String type = child.getStringAttribute("type"); 3 try { 4 Class<?> clazz = Resources.classForName(type); 5 if (alias == null) { 6 typeAliasRegistry.registerAlias(clazz); 7 } else { 8 typeAliasRegistry.registerAlias(alias, clazz); 9 } 10 } catch (ClassNotFoundException e) { 11 throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); 12 }
這裡先解析<typeAlias>中的alias屬性,再解析<typeAlias>中的type屬性,當然alias也可以不定義,不定義走的就是第6行的registerAlias方法,定義走的就是第8行的registerAlias方法,這兩個過載的registerAlias方法前面也都說過了,就不說了。
預設typeAlias
上面說的是自定義typeAlias,MyBatis本身也預設提供給開發者了一些typeAlias定義,在兩處地方。第一處地方在Configuration的構造方法中:
public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); ... }
第二處地方是在TypeAliasRegistry的構造方法中:
1 public TypeAliasRegistry() { 2 registerAlias("string", String.class); 3 4 registerAlias("byte", Byte.class); 5 registerAlias("long", Long.class); 6 registerAlias("short", Short.class); 7 registerAlias("int", Integer.class); 8 registerAlias("integer", Integer.class); 9 registerAlias("double", Double.class); 10 registerAlias("float", Float.class); 11 registerAlias("boolean", Boolean.class); 12 13 registerAlias("byte[]", Byte[].class); 14 registerAlias("long[]", Long[].class); 15 registerAlias("short[]", Short[].class); 16 registerAlias("int[]", Integer[].class); 17 registerAlias("integer[]", Integer[].class); 18 registerAlias("double[]", Double[].class); 19 registerAlias("float[]", Float[].class); 20 registerAlias("boolean[]", Boolean[].class); 21 22 registerAlias("_byte", byte.class); 23 registerAlias("_long", long.class); 24 registerAlias("_short", short.class); 25 registerAlias("_int", int.class); 26 registerAlias("_integer", int.class); 27 registerAlias("_double", double.class); 28 registerAlias("_float", float.class); 29 registerAlias("_boolean", boolean.class); 30 31 registerAlias("_byte[]", byte[].class); 32 registerAlias("_long[]", long[].class); 33 registerAlias("_short[]", short[].class); 34 registerAlias("_int[]", int[].class); 35 registerAlias("_integer[]", int[].class); 36 registerAlias("_double[]", double[].class); 37 registerAlias("_float[]", float[].class); 38 registerAlias("_boolean[]", boolean[].class); 39 40 registerAlias("date", Date.class); 41 registerAlias("decimal", BigDecimal.class); 42 registerAlias("bigdecimal", BigDecimal.class); 43 registerAlias("biginteger", BigInteger.class); 44 registerAlias("object", Object.class); 45 46 registerAlias("date[]", Date[].class); 47 registerAlias("decimal[]", BigDecimal[].class); 48 registerAlias("bigdecimal[]", BigDecimal[].class); 49 registerAlias("biginteger[]", BigInteger[].class); 50 registerAlias("object[]", Object[].class); 51 52 registerAlias("map", Map.class); 53 registerAlias("hashmap", HashMap.class); 54 registerAlias("list", List.class); 55 registerAlias("arraylist", ArrayList.class); 56 registerAlias("collection", Collection.class); 57 registerAlias("iterator", Iterator.class); 58 59 registerAlias("ResultSet", ResultSet.class); 60 }
對於這些資料,我們可以直接使用registerAlias方法的第一個引數對應的字串而不需要定義這些typeAlias。