如果我們要使用MyBatis進行數據庫操作的話,大致要做兩件事情:
- 定義dao接口文件
在dao接口中定義需要進行的數據庫操作方法。 - 創建映射文件
當有了dao接口后,還需要為該接口創建映射文件。映射文件中定義了一系列SQL語句,這些SQL語句和dao接口一一對應。
MyBatis在初始化的時候會將映射文件與dao接口一一對應,並根據映射文件的內容為每個函數創建相應的數據庫操作能力。而我們作為MyBatis使用者,只需將dao接口注入給Service層使用即可。
那么MyBatis是如何根據映射文件為每個dao接口創建具體實現的?答案是——動態代理。
1、解析mapper文件
啟動時加載解析mapper的xml
如果不是集成spring的,會去讀取<mappers>節點,去加載mapper的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> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="multipleResultSetsEnabled" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="useGeneratedKeys" value="false"/> <setting name="defaultExecutorType" value="SIMPLE"/> <setting name="defaultStatementTimeout" value="2"/> </settings> <typeAliases> <typeAlias alias="CommentInfo" type="com.xixicat.domain.CommentInfo"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/demo"/> <property name="username" value="root"/> <property name="password" value=""/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/xixicat/dao/CommentMapper.xml"/> </mappers> </configuration>
如果是集成spring的,會去讀spring的sqlSessionFactory的xml配置中的mapperLocations,然后去解析mapper的xml
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 配置mybatis配置文件的位置 --> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="typeAliasesPackage" value="com.xixicat.domain"/> <!-- 配置掃描Mapper XML的位置 --> <property name="mapperLocations" value="classpath:com/xixicat/dao/*.xml"/> </bean>
mapper節點解析過程
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse();
由上述代碼可知,解析mapper節點的解析是由XMLMapperBuilder類的parse()函數來完成的,下面我們就詳細看一下parse()函數。
public void parse() { // 若當前Mapper.xml尚未加載,則加載 if (!configuration.isResourceLoaded(resource)) { // 解析<mapper>節點 configurationElement(parser.evalNode("/mapper")); // 將當前Mapper.xml標注為『已加載』(下回就不用再加載了) configuration.addLoadedResource(resource); // 【關鍵】將Mapper Class添加至Configuration中 bindMapperForNamespace(); //綁定namespace } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
這個函數主要做了兩件事:
- 解析
<mapper>節點,並將解析結果注冊進configuration中; - 將當前映射文件所對應的DAO接口的Class對象注冊進
configuration中
這一步極為關鍵!是為了給DAO接口創建代理對象,下文會詳細介紹。
下面再進入bindMapperForNamespace()函數,看一看它做了什么:
private void bindMapperForNamespace() { // 獲取當前映射文件對應的DAO接口的全限定名 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { // 將全限定名解析成Class對象 Class<?> boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // 將當前Mapper.xml標注為『已加載』(下回就不用再加載了) configuration.addLoadedResource("namespace:" + namespace); // 將DAO接口的Class對象注冊進configuration中 configuration.addMapper(boundType); } } } }
這個函數主要做了兩件事:
- 將
<mapper>節點上定義的namespace屬性(即:當前映射文件所對應的DAO接口的權限定名)解析成Class對象 - 將該Class對象存儲在
configuration對象里的屬性MapperRegistry中,見下代碼
MapperRegistry mapperRegistry = new MapperRegistry(this) public <T> void addMapper(Class<T> type) { this.mapperRegistry.addMapper(type); }
我們繼續進入MapperRegistry
public class MapperRegistry { private final Configuration config; private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); }
MapperRegistry有且僅有兩個屬性:Configuration和knownMappers。
其中,knownMappers的類型為Map<Class<?>, MapperProxyFactory<?>>,它是一個Map,key為dao接口的class對象,而value為該dao接口代理對象的工廠。那么這個代理對象工廠是什么,又怎么產生的呢?我們先來看一下MapperRegistry的addMapper()函數。
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 創建MapperProxyFactory對象,並put進knownMappers中 knownMappers.put(type, new MapperProxyFactory<T>(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
從這個函數可知,MapperProxyFactory是在這里創建,並put進knownMappers中的。
下面我們就來看一下MapperProxyFactory這個類究竟有些啥:
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
這個類有三個重要成員:
- mapperInterface屬性
這個屬性就是DAO接口的Class對象,當創建MapperProxyFactory對象的時候需要傳入 - methodCache屬性
這個屬性用於存儲當前DAO接口中所有的方法。 - newInstance函數
這個函數用於創建DAO接口的代理對象,它需要傳入一個MapperProxy對象作為參數。而MapperProxy類實現了InvocationHandler接口,由此可知它是動態代理中的處理類,所有對目標函數的調用請求都會先被這個處理類截獲,所以可以在這個處理類中添加目標函數調用前、調用后的邏輯。
2、DAO函數調用過程
configuration對象中存儲了所有DAO接口的Class對象和相應的
MapperProxyFactory對象(用於創建DAO接口的代理對象)。接下來,就到了使用DAO接口中函數的階段了。
SqlSession sqlSession = sqlSessionFactory.openSession(); try { ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class); List<Product> productList = productMapper.selectProductList(); for (Product product : productList) { System.out.printf(product.toString()); } } finally { sqlSession.close(); }
我們首先需要從sqlSessionFactory對象中創建一個SqlSession對象,然后調用sqlSession.getMapper(ProductMapper.class)來獲取代理對象。
我們先來看一下sqlSession.getMapper()是如何創建代理對象的?
public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }
sqlSession.getMapper()調用了configuration.getMapper(),那我們再看一下configuration.getMapper():
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
configuration.getMapper()又調用了mapperRegistry.getMapper(),那好,我們再深入看一下mapperRegistry.getMapper():
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
看到這里我們就恍然大悟了,原來它根據上游傳遞進來DAO接口的Class對象,從configuration中取出了該DAO接口對應的代理對象生成工廠:MapperProxyFactory;
在有了這個工廠后,再通過newInstance函數創建該DAO接口的代理對象,並返回給上游。
OK,此時我們已經獲取了代理對象,接下來就可以使用這個代理對象調用相應的函數了。
SqlSession sqlSession = sqlSessionFactory.openSession(); try { ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class); List<Product> productList = productMapper.selectProductList(); } finally { sqlSession.close(); }
以上述代碼為例,當我們獲取到ProductMapper的代理對象后,我們調用了它的selectProductList()函數。
3、下面我們就來分析下代理函數調用過程。
當調用了代理對象的某一個代理函數后,這個調用請求首先會被發送給代理對象處理類MapperProxy的invoke()函數,這個類繼承了InvocationHandler:
//這里會攔截Mapper接口的所有方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { //如果是Object中定義的方法,直接執行。如toString(),hashCode()等 try { return method.invoke(this, args);// } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } final MapperMethod mapperMethod = cachedMapperMethod(method); //其他Mapper接口定義的方法交由mapperMethod來執行 return mapperMethod.execute(sqlSession, args); }
這個方法中,首先會過濾object中的通用方法,遇到object方法會直接執行;
但是如果是非通用方法,現執行cachedMapperMethod(method):從當前代理對象處理類MapperProxy的methodCache屬性中獲取method方法的詳細信息(即:MapperMethod對象)。如果methodCache中沒有就創建並加進去。
有了MapperMethod對象后執行它的execute()方法,該方法就會調用JDBC執行相應的SQL語句,並將結果返回給上游調用者。至此,代理對象函數的調用過程結束!
MapperMethod類是統管所有和數據庫打交道的方法。所以,不管你的dao層有多少方法,歸結起來的sql語句都有且僅有只有insert、delete、update、select,可以預料在MapperMethod的execute方法中首先判斷是何種sql語句。
1 /** 2 * 這個方法是對SqlSession的包裝,對應insert、delete、update、select四種操作 3 */ 4 public Object execute(SqlSession sqlSession, Object[] args) { 5 Object result;//返回結果 6 //INSERT操作 7 if (SqlCommandType.INSERT == command.getType()) { 8 Object param = method.convertArgsToSqlCommandParam(args); 9 //調用sqlSession的insert方法 10 result = rowCountResult(sqlSession.insert(command.getName(), param)); 11 } else if (SqlCommandType.UPDATE == command.getType()) { 12 //UPDATE操作 同上 13 Object param = method.convertArgsToSqlCommandParam(args); 14 result = rowCountResult(sqlSession.update(command.getName(), param)); 15 } else if (SqlCommandType.DELETE == command.getType()) { 16 //DELETE操作 同上 17 Object param = method.convertArgsToSqlCommandParam(args); 18 result = rowCountResult(sqlSession.delete(command.getName(), param)); 19 } else if (SqlCommandType.SELECT == command.getType()) { 20 //如果返回void 並且參數有resultHandler ,則調用 void select(String statement, Object parameter, ResultHandler handler);方法 21 if (method.returnsVoid() && method.hasResultHandler()) { 22 executeWithResultHandler(sqlSession, args); 23 result = null; 24 } else if (method.returnsMany()) { 25 //如果返回多行結果,executeForMany這個方法調用 <E> List<E> selectList(String statement, Object parameter); 26 result = executeForMany(sqlSession, args); 27 } else if (method.returnsMap()) { 28 //如果返回類型是MAP 則調用executeForMap方法 29 result = executeForMap(sqlSession, args); 30 } else { 31 //否則就是查詢單個對象 32 Object param = method.convertArgsToSqlCommandParam(args); 33 result = sqlSession.selectOne(command.getName(), param); 34 } 35 } else { 36 //接口方法沒有和sql命令綁定 37 throw new BindingException("Unknown execution method for: " + command.getName()); 38 } 39 //如果返回值為空 並且方法返回值類型是基礎類型 並且不是void 則拋出異常 40 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { 41 throw new BindingException("Mapper method '" + command.getName() 42 + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 43 } 44 return result; 45 }
我們選取第26行中的executeForMany中的方法來解讀試試看。
1 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 2 List<E> result; 3 Object param = method.convertArgsToSqlCommandParam(args); 4 if (method.hasRowBounds()) { //是否分頁查詢,RowBounds中有兩個數字,offset和limit 5 RowBounds rowBounds = method.extractRowBounds(args); 6 result = sqlSession.<E>selectList(command.getName(), param, rowBounds); 7 } else { 8 result = sqlSession.<E>selectList(command.getName(), param); 9 } 10 // issue #510 Collections & arrays support 11 if (!method.getReturnType().isAssignableFrom(result.getClass())) { 12 if (method.getReturnType().isArray()) { 13 return convertToArray(result); 14 } else { 15 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 16 } 17 } 18 return result; 19 }
第6行和第8行代碼就是我們真正執行sql語句的地方,原來兜兜轉轉它又回到了sqlSession的方法中。DefaultSqlSession是SqlSession的實現類,所以我們重點關注DefaultSqlSession類。
public <E> List<E> selectList(String statement) { return this.selectList(statement, null); } public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); return result; } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
我們看到關於selectList有三個重載方法,最后調用的都是public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)。在此方法中第一個參數為String類型且命名為statement,第二個參數為Object命名為parameter,回到MapperMethod的executeForMany方法,可以看到傳遞了什么參數進來,跟蹤command.getName()是怎么來的。
private final SqlCommand command;
看來是一個叫做SqlCommand的變量,進而我們可以發現這個SqlCommand是MapperMethod的靜態內部類。
1 //org.apache.ibatis.binding.MapperMethod 2 public static class SqlCommand { 3 4 private final String name; 5 private final SqlCommandType type; 6 7 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { 8 String statementName = mapperInterface.getName() + "." + method.getName(); //獲取接口+方法名 9 MappedStatement ms = null; //定義一個MappedStatement,這個MapperedStatement稍后介紹 10 if (configuration.hasStatement(statementName)) { //從Configuration對象查找是否有這個方法的全限定名稱 11 ms = configuration.getMappedStatement(statementName); //有,則根據方法的全限定名稱獲取MappedStatement實例 12 } else if (!mapperInterface.equals(method.getDeclaringClass())) { //如果沒有在Configuration對象中找到這個方法,則向上父類中獲取全限定方法名 13 String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); 14 if (configuration.hasStatement(parentStatementName)) { //在向上的父類查找是否有這個全限定方法名 15 ms = configuration.getMappedStatement(parentStatementName); //有,則根據方法的全限定名稱獲取MappedStatement實例 16 } 17 } 18 if (ms == null) { 19 if(method.getAnnotation(Flush.class) != null){ 20 name = null; 21 type = SqlCommandType.FLUSH; 22 } else { 23 throw new BindingException("Invalid bound statement (not found): " + statementName); 24 } 25 } else { 26 name = ms.getId(); //這個ms.getId,其實就是我們在mapper.xml配置文件中配置一條sql語句時開頭的<select id="……"……,即是接口的該方法名全限定名稱 27 type = ms.getSqlCommandType(); //顯然這是將sql是何種類型(insert、update、delete、select)賦給type 28 if (type == SqlCommandType.UNKNOWN) { 29 throw new BindingException("Unknown execution method for: " + name); 30 } 31 } 32 } 33 34 public String getName() { 35 return name; 36 } 37 38 public SqlCommandType getType() { 39 return type; 40 } 41 }
大致對MapperMethod的解讀到此,再次回到DefaultSqlSession中,走到核心的這句
MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
在獲取到MappedStatement后,碰到了第一個SqlSession下的四大對象之一:Executor執行器。關於executor我們在下一篇文章中解析。
