我們在使用MyBatis執行查詢語句的時候,通常都會有一個返回類型,這個是在mapper文件中給sql增加一個resultType(或resultMap)屬性進行控制。resultType和resultMap都能控制返回類型,只要定義了這個配置就能自動返回我想要的結果,於是我就很納悶這個自動過程的實現原理,想必大多數人剛開始的時候應該也有和我一樣的困惑和好奇,那么今天我就把自己的研究分享一下。在JDBC中查詢的結果會保存在一個結果集中,其實MyBatis也是這個原理,只不過MyBatis在創建結果集的時候,會使用其定義的對象工廠DefaultObjectFactory來完成對應的工作,下面來看一下其源代碼:
一、默認對象工廠DefaultObjectFactory.java
1 /** 2 * @author Clinton Begin 3 */ 4 public class DefaultObjectFactory implements ObjectFactory, Serializable { 5 6 private static final long serialVersionUID = -8855120656740914948L; 7 8 @Override 9 public <T> T create(Class<T> type) { 10 return create(type, null, null); 11 } 12 13 @SuppressWarnings("unchecked") 14 @Override 15 public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { 16 Class<?> classToCreate = resolveInterface(type); 17 // we know types are assignable 18 return (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs); 19 } 20 21 @Override 22 public void setProperties(Properties properties) { 23 // no props for default 24 } 25 26 private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { 27 try { 28 Constructor<T> constructor; 29 if (constructorArgTypes == null || constructorArgs == null) { 30 constructor = type.getDeclaredConstructor(); 31 if (!constructor.isAccessible()) { 32 constructor.setAccessible(true); 33 } 34 return constructor.newInstance(); 35 } 36 constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()])); 37 if (!constructor.isAccessible()) { 38 constructor.setAccessible(true); 39 } 40 return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()])); 41 } catch (Exception e) { 42 StringBuilder argTypes = new StringBuilder(); 43 if (constructorArgTypes != null && !constructorArgTypes.isEmpty()) { 44 for (Class<?> argType : constructorArgTypes) { 45 argTypes.append(argType.getSimpleName()); 46 argTypes.append(","); 47 } 48 argTypes.deleteCharAt(argTypes.length() - 1); // remove trailing , 49 } 50 StringBuilder argValues = new StringBuilder(); 51 if (constructorArgs != null && !constructorArgs.isEmpty()) { 52 for (Object argValue : constructorArgs) { 53 argValues.append(String.valueOf(argValue)); 54 argValues.append(","); 55 } 56 argValues.deleteCharAt(argValues.length() - 1); // remove trailing , 57 } 58 throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e); 59 } 60 } 61 62 protected Class<?> resolveInterface(Class<?> type) { 63 Class<?> classToCreate; 64 if (type == List.class || type == Collection.class || type == Iterable.class) { 65 classToCreate = ArrayList.class; 66 } else if (type == Map.class) { 67 classToCreate = HashMap.class; 68 } else if (type == SortedSet.class) { // issue #510 Collections Support 69 classToCreate = TreeSet.class; 70 } else if (type == Set.class) { 71 classToCreate = HashSet.class; 72 } else { 73 classToCreate = type; 74 } 75 return classToCreate; 76 } 77 78 @Override 79 public <T> boolean isCollection(Class<T> type) { 80 return Collection.class.isAssignableFrom(type); 81 } 82 83 }
在這個類中有6個方法:
1⃣️create(Class<T> type):這個方法我認為是創建結果集的方法,但它其實是直接調用了第二個create方法,只不過后兩個參數傳的是null,所以直接看下面的方法;
2⃣️create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs):這個方法中傳了三個參數,分別應該是返回的結果集類型、此類構造函數參數類型、此類構造函數參數值。從它的內部實現來看,首先調用了下面的resolveInterface方法獲取返回類型,其次調用instantiateClass方法實例化出我們所需的結果集;
3⃣️setProperties(Properties properties):這個方法是用來設置一些配置信息,比如我們給objectFactory定義一個property子元素時,就是通過這個方法進行配置的;
4⃣️instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs):此方法是用來實例化一個類,需要實例化的類型是通過下面的resolveInterface方法決定,從內部實現來看,這個實例化過程是通過反射實現的;
5⃣️resolveInterface(Class<?> type):此方法是用來對集合類型進行處理,即如果我們定義一個resultType為集合類型,那么它就會根據這個類型決定出即將創建的結果集類型;
6⃣️isCollection(Class<T> type):這個方法是用來判斷我們配置的類型是不是一個集合。比如如果返回多條數據,但是我們配置resultType是個普通類,那么在執行過程中就會報錯;
以上就是MyBatis默認objectFactory中的具體實現,通過它來創建我們配置的結果集,一般情況下都會使用默認的對象工廠,但是我們也可以自定義一個,只要繼承DefaultObjectFactory.java即可。
二、自定義一個對象工廠MyObjectFactory.java
1 package com.daily.objectfactory; 2 3 import java.util.List; 4 import java.util.Properties; 5 6 import org.apache.ibatis.reflection.factory.DefaultObjectFactory; 7 8 public class MyObjectFactory extends DefaultObjectFactory{ 9 10 private static final long serialVersionUID = 1L; 11 12 @Override 13 public void setProperties(Properties properties) { 14 super.setProperties(properties); 15 System.out.println("初始化參數:["+properties.toString()+"]"); 16 } 17 18 19 @Override 20 public <T> T create(Class<T> type,List<Class<?>> constructorArgTypes,List<Object> constructorArgs) { 21 T result = super.create(type, constructorArgTypes, constructorArgs); 22 System.out.println("創建對象方法:"+result.getClass().getSimpleName()); 23 return result; 24 } 25 26 @Override 27 public <T> boolean isCollection(Class<T> type) { 28 return super.isCollection(type); 29 } 30 31 }
其實我們自定義的時候只要重寫其中的create方法就可以,至於選擇哪個,其實兩個都行,因為第一個方法內部還是調用了第二個,我在這里調用了第二個,還重寫了setProperties方法,用來查看其作用。
下面據一個例子來查看怎么使用自定義的objectFactory
第一步:配置
在mybatis-config.xml中做如下配置,並自定義一個property子元素,設置其name和value
1 <!--對象工廠 --> 2 <objectFactory type="com.daily.objectfactory.MyObjectFactory"> 3 <property name="prop1" value="value1"/> 4 </objectFactory>
第二步:配置查詢映射器接口和sql
接口:
1 //獲取所有的產品 2 public List<Product> getAllProduct();
sql:
1 <select id="getAllProduct"resultMap="BaseResultMap"> 2 SELECT * FROM product 3 </select>
在上面的代碼中,接口返回的是一個List,而其中的元素是Product類,sql定義返回的結果集是一個BaseResultMap,其定義如下:
1 <resultMap id="BaseResultMap" type="com.daily.pojo.Product"> 2 <id column="id" jdbcType="VARCHAR" property="id" /> 3 <result column="product_name" jdbcType="VARCHAR" property="productName" /> 4 <result column="product_price" jdbcType="VARCHAR" property="productPrice" /> 5 <result column="product_type" jdbcType="VARCHAR" property="productType" /> 6 </resultMap>
它們都在同一個mapper.xml文件中.
第三步:打印查詢結果
1 // 獲取商品列表 2 public void getProductList() { 3 List<Product> productList = productService.getProductList(); 4 for (Product product : productList) { 5 System.out.println(product.getProductName()); 6 } 7 }
獲取到商品列表之后進行遍歷並打印名稱
第四步:查詢結果
1 初始化參數:[{prop1=value1}] 2 創建對象方法:ArrayList 3 創建對象方法:Product 4 創建對象方法:Product 5 創建對象方法:Product 6 創建對象方法:Product 7 襯衫 8 衛衣 9 連衣裙 10 連衣裙
從執行結果來看,創建結果集的步驟如下:
1⃣️首先進行初始化參數,也就是根據objectFactory中的配置;
2⃣️然后創建了一個ArrayList對象;
3⃣️再創建四個Product對象(注意:查詢結果有幾條就創建幾個 );
4⃣️將四個對象設置到ArrayList中封裝成我們需要的返回類型List;
按照我的理解,mybatis創建結果集的過程是這樣的:首先根據sql執行的結果,即返回條數判斷是不是一個集合類型,然后將每條結果實例化成一個對象,如果前面判斷返回的是多條,則將這些對象設置到一個List中,否則就是一個單獨的對象,然后根據我們在映射器接口中定義的返回類型進行適配,如果接口定義返回List就返回List,如果接口定義返回一個單獨的類型,則返回實例化的對象。后來仔細一想不對呀,這樣的話如果查詢結果是一條,但是我在接口中定義的返回類型是List,那豈不是沖突了?為了研究這個創建過程,我又寫了兩個查詢接口,一個只返回一個對象,一個查詢所有但返回一個對象(這個按理說是不對的,主要看看mybatis會報什么錯),然后在DefaultObjectFactory中打斷點進行調試,下面介紹我的實驗過程:
第一步:再創建兩個查詢接口
1⃣️根據ID查詢
1 //查詢接口 2 public Product selectById(String id);
sql配置
1 <select id="selectById" parameterType="String" resultType="product"> 2 SELECT * FROM product p WHERE p.id = #{id,jdbcType=VARCHAR} 3 </select>
這個是根據ID查詢,返回類型是Product
2⃣️查詢所有
1 public Product getAllProducts();
sql配置
1 <select id="getAllProducts" resultType="product"> 2 SELECT * FROM product 3 </select>
這個是查詢所有,但返回類型是Product,即一個對象
第二步:對三種情況打斷點進行調試
在調試過程中我發現以下幾點:
1、在獲取SQL session對象的過程中就已經調用了DefaultObjectFactory中的setProperties方法,其實這個不難理解,畢竟在獲取SQL session之前就已經加載了mybatis的配置文件,首先當然要進行配置;
2、然后會進入到DefaultObjectFactory中的isCollection方法,而傳給它的參數類型就是我在映射器接口中定義的返回類型;
3、不管查詢出多少條數據,也不管返回類型是什么,首先都會調用create方法創建一個ArrayList對象;
4、查詢出多少條數據,就會調用多少次create方法,將一條數據封裝成一個類並進行實例化;
以上是三種不同的查詢結果都會執行的步驟,其他的中間還有很復雜的創建過程,根據我有限的理解,推斷出mybatis在創建結果集的過程如下:
1⃣️先判斷接口返回的類型是一個單獨的類還是一個集合;
2⃣️創建一個ArrayList對象;
3⃣️將每個查詢結果實例化成一個對象,然后設置到ArrayList對象中;
4⃣️如果在第一步中接口返回的是單獨的類,此時查看第三步封裝的結果,如果只有一條數據,則獲取對象並返回;如果有多條,則報錯;
如果在第一步中接口返回的是一個集合,則直接返回第三步封裝的結果;
其實所謂結果集,必定是個集合,所以才會有創建ArrayList並將實例化的對象添加在其中這一步,至於怎么返回就要看接口中的定義了,如果mybatis能根據接口定義返回指定類型,則沒有任何問題,否則就會報錯,就像上面的第三種情況,封裝的結果集是個含多個product對象的集合,但是接口中定義只要一個對象,那這時候mybatis就不知道給哪個合適,所以只能報錯了。
以上就是我對objectFactory這個配置項的理解,不正確的地方希望看到文章的你能給予指正,謝了~~