mybatis源碼配置文件解析之二:解析settings標籤
在前邊的部落格中分析了mybatis解析properties標籤,《mybatis源碼配置文件解析之一:解析properties標籤》。下面來看解析settings標籤的過程。
一、概述
在mybatis的核心配置文件(mybatis-config.xml)文件中,有關於settings標籤的配置,如下
<settings> <!-- 設置日誌輸出為LOG4J --> <setting name="logImpl" value="STDOUT_LOGGING" /> <!--將以下畫線方式命名的資料庫列映射到 Java 對象的駝峰式命名屬性中--> <setting name= "mapUnderscoreToCamelCase" value="true" /> </settings>
上面只簡單的給出settings標籤的配置,settings標籤配置在<configuration>標籤中,是<configuration>標籤的子標籤。在settings標籤中可以配置setting子標籤,上面是我的一個配置,是以name-value鍵值對的放式進行配置。這裡有個問題setting標籤中的name怎麼配置,共有多少配置?
二、詳述
上面,看到了settings標籤的配置方式,下面看其解析過程,在XMLConfigBuilder類中的parseConfiguration方法中有關於該標籤的解析,
private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析properties標籤 propertiesElement(root.evalNode("properties")); //解析settings標籤,1、把<setting>標籤解析為Properties對象 Properties settings = settingsAsProperties(root.evalNode("settings")); /*2、對<settings>標籤中的<setting>標籤中的內容進行解析,這裡解析的是<setting name="vfsImpl" value=","> * VFS是mybatis中用來表示虛擬文件系統的一個抽象類,用來查找指定路徑下的資源。上面的key為vfsImpl的value可以是VFS的具體實現,必須 * 是許可權類名,多個使用逗號隔開,如果存在則設置到configuration中的vfsImpl屬性中,如果存在多個,則設置到configuration中的僅是最後一個 * */ loadCustomVfs(settings); //解析別名標籤,例<typeAlias alias="user" type="cn.com.bean.User"/> typeAliasesElement(root.evalNode("typeAliases")); //解析插件標籤 pluginElement(root.evalNode("plugins")); //解析objectFactory標籤,此標籤的作用是mybatis每次創建結果對象的新實例時都會使用ObjectFactory,如果不設置 //則默認使用DefaultObjectFactory來創建,設置之後使用設置的 objectFactoryElement(root.evalNode("objectFactory")); //解析objectWrapperFactory標籤 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //解析reflectorFactory標籤 reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 //解析environments標籤 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); //解析<mappers>標籤 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
上面便是parseConfiguration方法,在此方法中下面的方法對settings進行了解析,
//解析settings標籤,1、把<setting>標籤解析為Properties對象 Properties settings = settingsAsProperties(root.evalNode("settings"));
調用settingsAsProperties方法,從方法名中可以看出要把settings標籤中的內容解析到Proerties對象中,因為settings標籤中是name-value的配置,剛好解析到Properties中以鍵值對的形式存儲。下面是settingsAsProperties方法,
private Properties settingsAsProperties(XNode context) { if (context == null) { return new Properties(); } //把<setting name="" value="">標籤解析為Properties對象 Properties props = context.getChildrenAsProperties(); // Check that all settings are known to the configuration class MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); //如果獲取的配置的<setting name="" value="">資訊,name不在metaConfig中,則會拋出異常 //這裡metaConfig中的資訊是從Configuration類中解析出來的,包含set方法的屬性 //所以在配置<setting>標籤的時候,其name值可以參考configuration類中的屬性,配置為小寫 for (Object key : props.keySet()) { //從metaConfig的relector中的setMethods中判斷是否存在該屬性,setMethods中存儲的是可寫的屬性, //所以這裡要到setMethods中進行判斷 if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } return props; }
1、解析子標籤
解析子標籤也就是settings標籤中的setting標籤,使用下面的方法進行解析,
//把<setting name="" value="">標籤解析為Properties對象 Properties props = context.getChildrenAsProperties();
調用了getChildrenAsProperties方法,
public Properties getChildrenAsProperties() { Properties properties = new Properties(); for (XNode child : getChildren()) { String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); if (name != null && value != null) { properties.setProperty(name, value); } } return properties; }
該方法就是解析<settings></settings>標籤中的<setting></setting>標籤,取出標籤中的name和value屬性,存儲到Properties對象中且返回。
我們再看上面的settingsAsProperties方法,調用上述getChildrenAsProperties方法獲得Properties對象後又進行了其他操作。
2、校驗setting標籤中的name值是否存在
2.1、獲得setting標籤中的所有name值
在本文開篇提到一個問題,setting標籤中的name值怎麼配置,答案是可以參考mybatis的官方文檔,在官方文檔中有詳細的解釋,再有就是分析源碼,繼續往下看。
在settingsAsProperties方法中看下面一行程式碼,
// Check that all settings are known to the configuration class MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
上面這行程式碼就解析了setting標籤中的name可以配置的所有值。再看程式碼上的注釋,是不是豁然開朗。該方法有兩個參數,一個是Configuration.class,一個是localReflectorFactory,看localReflectorFactory,
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
使用了DefaultReflectorFactory,看其默認構造方法
默認構造方法僅初始化了classCacheEnabled和relectorMap兩個屬性。後過來繼續看MetaClass.forClass方法,
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) { return new MetaClass(type, reflectorFactory); }
該方法返回的是一個MetaClass的對象,
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) { this.reflectorFactory = reflectorFactory; this.reflector = reflectorFactory.findForClass(type); }
重點看reflectorFactory.findForClass方法,這裡reflectorFactory是DefaultReflectorFactory的一個實例。下面是DefaultReflectorFactory的findForClass方法,
@Override public Reflector findForClass(Class<?> type) { if (classCacheEnabled) { // synchronized (type) removed see issue #461 Reflector cached = reflectorMap.get(type); if (cached == null) { cached = new Reflector(type); reflectorMap.put(type, cached); } return cached; } else { return new Reflector(type); } }
上面方法中,重點看new Reflector(type)這句方法,
public Reflector(Class<?> clazz) { type = clazz; //解析默認的構造方法,及無參構造方法 addDefaultConstructor(clazz); //解析clazz中的get方法,這裡的clazz指的是Configuration.class addGetMethods(clazz); //解析clazz中的set方法,這裡的clazz指的是Configuration.class addSetMethods(clazz); addFields(clazz); readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); for (String propName : readablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } for (String propName : writeablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } }
此方法完成的功能是解析clazz(包含其父類)的構造方法、getXX方法、setXX方法、欄位,通過一個類的Class對象獲取。
addDefaultConstructor(clazz)如下,
private void addDefaultConstructor(Class<?> clazz) { //獲得該類的聲明的構造方法 Constructor<?>[] consts = clazz.getDeclaredConstructors(); //對構造方法進行循環 for (Constructor<?> constructor : consts) { //判斷構造方法的參數是否為0,為0代表為默認的無參構造方法 if (constructor.getParameterTypes().length == 0) { //如果是私有的(修飾符為private),這裡需要設置可見。 if (canAccessPrivateMethods()) { try { constructor.setAccessible(true); } catch (Exception e) { // Ignored. This is only a final precaution, nothing we can do. } } if (constructor.isAccessible()) { this.defaultConstructor = constructor; } } } }
上面方法獲得傳入的Class對象所以構造方法,把默認的無參構造方法賦給defaultConstructor。
addGetMethods(clazz)如下,
private void addGetMethods(Class<?> cls) { Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>(); //使用反射的放上獲得cls的所有方法 Method[] methods = getClassMethods(cls); //把所有的方法放入conflictingGetters中,key為屬性名,value為List<Method> for (Method method : methods) { //方法的參數大於0,則結束本次循環,因為這裡解析的是get方法,get方法默認不應該有參數 if (method.getParameterTypes().length > 0) { continue; } String name = method.getName(); //如果以get或is開頭,且方法名稱分別大於3和2,則說明是get方法 if ((name.startsWith("get") && name.length() > 3) || (name.startsWith("is") && name.length() > 2)) { //通過方法名轉化為屬性名,如,getUserName--userName name = PropertyNamer.methodToProperty(name); addMethodConflict(conflictingGetters, name, method); } }
/**處理一個屬性多個get方法的情況,即conflictingGetter方法中一個key對應的value的長度大於1的情況,如下
*key propertyName
*value list<Method> 其長度大於1
*/
resolveGetterConflicts(conflictingGetters);
}
獲取所有以get和is開頭的方法,調用addMethodConflict方法,這裡的方法名直譯過來是添加衝突的方法,這裡衝突怎麼理解,我們看addMethodConflict方法,
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) { //根據欄位名取方法 List<Method> list = conflictingMethods.get(name); if (list == null) { list = new ArrayList<Method>(); conflictingMethods.put(name, list); } list.add(method); }
這裡是根據get和is開頭的方法獲取屬性名作為鍵值,並且使用list作為value進行存儲,為什麼使用list那,我們看下面的方法
public void getUser(){} public User getuser(){} public List<User> getUser(){}
public void getUser(String id){}
上面三個方法都會以user為鍵進行存儲,但是其方法名是一樣的,所以這裡要存儲為list,即存儲多個Method對象。
我們知道一個欄位的屬性的get或set方法,不可能出現上面的情況,所以針對上面的情況需要做處理,這裡調用resolveGetterConflicts(conflicttingGetters),
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) { //遍歷conflictingGetters for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) { Method winner = null; String propName = entry.getKey(); //循環value這裡value是一個List<Method>類型 for (Method candidate : entry.getValue()) { if (winner == null) { winner = candidate; continue; } //獲得get方法的返回值類型 Class<?> winnerType = winner.getReturnType(); Class<?> candidateType = candidate.getReturnType(); //如果winnerType和candidateType相等, if (candidateType.equals(winnerType)) { if (!boolean.class.equals(candidateType)) { throw new ReflectionException( "Illegal overloaded getter method with ambiguous type for property " + propName + " in class " + winner.getDeclaringClass() + ". This breaks the JavaBeans specification and can cause unpredictable results."); } else if (candidate.getName().startsWith("is")) { winner = candidate; } } else if (candidateType.isAssignableFrom(winnerType)) { // OK getter type is descendant } else if (winnerType.isAssignableFrom(candidateType)) { winner = candidate; } else { throw new ReflectionException( "Illegal overloaded getter method with ambiguous type for property " + propName + " in class " + winner.getDeclaringClass() + ". This breaks the JavaBeans specification and can cause unpredictable results."); } } addGetMethod(propName, winner); } }
上面的方法處理了上面提到的一個屬性存在多個get方法的情況,最後調用addGetMethod方法,
private void addGetMethod(String name, Method method) { if (isValidPropertyName(name)) { getMethods.put(name, new MethodInvoker(method)); Type returnType = TypeParameterResolver.resolveReturnType(method, type); getTypes.put(name, typeToClass(returnType)); } }
上面的方法把資訊放到了getMethods和getTyps中,分別存儲了get方法和返回值。
上面分析了Reflector中的addGetMethods方法,addSetMethods方法和其處理過程類似,最終把set方法和返回值放到了setMethods和setTypes中。
addFileds(clazz)方法即是處理clazz中的屬性,
private void addFields(Class<?> clazz) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (canAccessPrivateMethods()) { try { field.setAccessible(true); } catch (Exception e) { // Ignored. This is only a final precaution, nothing we can do. } } if (field.isAccessible()) { //檢查是否存在set方法,如果不存在添加該field if (!setMethods.containsKey(field.getName())) { // issue #379 - removed the check for final because JDK 1.5 allows // modification of final fields through reflection (JSR-133). (JGB) // pr #16 - final static can only be set by the classloader int modifiers = field.getModifiers(); if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) { addSetField(field); } } //檢查是否存在get方法,如果不存在添加該field if (!getMethods.containsKey(field.getName())) { addGetField(field); } } } //添加父類的field if (clazz.getSuperclass() != null) { addFields(clazz.getSuperclass()); } }
獲得field之後,判斷是否在getMethods和setMethods中,如果不在則進行添加,只看addSetField方法,
private void addSetField(Field field) { if (isValidPropertyName(field.getName())) { setMethods.put(field.getName(), new SetFieldInvoker(field)); Type fieldType = TypeParameterResolver.resolveFieldType(field, type); setTypes.put(field.getName(), typeToClass(fieldType)); } }
從上面看到如果一個field不存在set方法,則生成一個SetFieldInvoker把該對象放入setMethods,從這裡可以看出一個setting配置的name值在configuration中可以沒有set方法。同理也可以沒有get方法。
上面分析完了settingsAsProperties方法中的下面這行程式碼,
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
把Configuration中的構造方法、get方法、set方法、field放入了metaConfig中的reflector對象中的下列屬性
private final String[] readablePropertyNames; private final String[] writeablePropertyNames; private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>(); private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>(); private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>(); private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>(); private Constructor<?> defaultConstructor;
2.2、校驗配置的setting標籤中的name是否存在
上面分析完了MetaClass.forClass方法,下面看如何對setting標籤配置的name進行校驗
for (Object key : props.keySet()) { //從metaConfig的relector中的setMethods中判斷是否存在該屬性,setMethods中存儲的是可寫的屬性, //所以這裡要到setMethods中進行判斷 if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } }
遍歷從setting標籤解析出來的Properties對象,調用metaConfig.hasSetter方法,
public boolean hasSetter(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); if (prop.hasNext()) { if (reflector.hasSetter(prop.getName())) { MetaClass metaProp = metaClassForProperty(prop.getName()); return metaProp.hasSetter(prop.getChildren()); } else { return false; } } else { return reflector.hasSetter(prop.getName()); } }
看hasSetter的定義
public boolean hasSetter(String propertyName) { return setMethods.keySet().contains(propertyName); }
可以看到是判斷setMethods是否存在該key,也就是已set方法為表標準,只要在setMethods中,便可以在<setting>標籤的name中配置,具體配置值還需要看其類型。
三、總結
上面分析了mybatis的核心配置文件中<settings>標籤的解析及子標籤中name屬性的配置值是怎麼取的。如果要擴展核心文件配置中的setting標籤的name屬性值,需要在configuration中進行配置,及其他操作。
原創不易,有不正之處歡迎指正。