1. 程式人生 > >mybatis原始碼學習:基於動態代理實現查詢全過程

mybatis原始碼學習:基於動態代理實現查詢全過程

前文傳送門: [mybatis原始碼學習:從SqlSessionFactory到代理物件的生成](https://www.cnblogs.com/summerday152/p/12773121.html) [mybatis原始碼學習:一級快取和二級快取分析](https://www.cnblogs.com/summerday152/p/12773135.html) 下面這條語句,將會呼叫代理物件的方法,並執行查詢過程,我們一起來看看它的內部是如何實現的。 ```java User user1 = userDao1.findById(41); ``` 一、動態代理:執行代理物件的方法時攔截,進行方法增強。 ```java /** * 作用:執行被代理物件的任何介面方法都會經過該方法 * @param proxy : 代理物件的引用 * @param method : 當前執行的方法 * @param args : 當前執行方法所需的引數 * @return : 和被代理物件有相同的返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //判斷它是否為類 if (Object.class.equals(method.getDeclaringClass())) { //如果是的話,直接呼叫該方法並返回 return method.invoke(this, args); } else if (isDefaultMethod(method)) { //判斷該方法是不是default方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //對msqlcommand和method進行封裝,並以method:mapperMethod的形式加入methodCache final MapperMethod mapperMethod = cachedMapperMethod(method); //返回mapperMethod的execute的返回結果 return mapperMethod.execute(sqlSession, args); } ``` 可以看看這個MapperMethod具體是個啥玩意兒: ```java //快取思想的體現 private MapperMethod cachedMapperMethod(Method method) { //從methodCache這個Map中取method對應的mapperMethod MapperMethod mapperMethod = methodCache.get(method); //如果裡面沒有,就建立一個 if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); //以method:mapperMethod的形式加入methodCache methodCache.put(method, mapperMethod); } //如果有就直接返回 return mapperMethod; } ``` MapperMethod的構造器,sqlCommand和methodSignature是他的兩個靜態內部類: ```java public MapperMethod(Class mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } ``` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425145348139.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) 二、接著執行MapperMethod物件的execute方法,其實原始碼還是通俗易懂的,無非就是按照不同的sql語句的類別進行不同的資料結果的封裝,值得注意的是,insert,update和delete其實底層都是呼叫了update方法,但為了語義清晰,所以區分類別。 之前command封裝了sql語句的類別,我們這是`SELECT`對吧, ```java public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { //將Args轉換為SqlCommand引數,簡單理解就是獲取了引數41,這裡就不深入了 Object param = method.convertArgsToSqlCommandParam(args); //呼叫selectOne方法,這部分可以發現,無論是使用代理dao還是定義sqlsession實現類,本質上都呼叫了這些方法,因為這裡的command。getName就是具體定義的sql的namespace.id result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } ``` 三、當然本例以findById為例,這裡呼叫的是SelectOne方法,接收`com.smday.dao.IUserDao.findById`和`41`。 ```java @Override public T selectOne(String statement, Object parameter) { //根據引數select List List list = this.selectList(statement, parameter); if (list.size() == 1) { //獲取列表的一個元素 return list.get(0); } else if (list.size() > 1) { //個數超過一丟擲異常 throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { //個數為0返回null return null; } } ``` 四、呼叫selectList的方法,實現如下: ```java @Override public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { //獲取MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); //wrapCollection方法是對集合型別或者陣列型別的引數做特殊處理 //通過執行器呼叫query方法 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } ``` 五、獲取MappedStatement物件,該物件代表一個增刪改查標籤的詳細資訊。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425145429304.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) 六、預設執行CachingExecutor.query(ms,xxx,x)方法,獲取boundsql,該物件包含sql的具體資訊,建立快取key。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425145440683.png) 七、先去二級快取中查詢資料,如果二級快取中沒有,則去一級快取(localCache)中查詢,接著資料庫(queryFromDatabase)一條龍服務,這部分就不贅述了。最終呼叫的是Executor的doQuery方法,`list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);`。 八、建立StatementHandler物件,預設為PreparedStatementHandler,用以操作statement執行操作。 > ps:StatementHandler定義了一些主要的方法:預編譯相關prepare、查詢query、設定引數parameterize等等。 ```java @Override public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { //從mappedStatement中獲取配置資訊物件 Configuration configuration = ms.getConfiguration(); //建立StatementHandler物件,處理sql語句的物件,預設為PreparedStatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); //建立prepareStatement物件 stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } ``` ```java public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //RoutingStatementHandler並不是真實的服務物件,將會通過介面卡模式找到對應的Statementhandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); //攔截鏈對方法進行攔截 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } ``` > Executor和Statement分為三種:Simple、Prepared、Callable。 > > SqlSession四大物件在建立的時候都會被攔截器進行攔截,我們之後再做學習。 九、在建立StatementHandler的時候,我們會發現,它還初始化建立了另外兩個重要的物件: ```java //用於引數處理 this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); //用於封裝結果集 this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); ``` 十、在建立prepareStatement物件的時候,其實還通過parameterHandler的prepare()對statement進行了引數的預編譯: ```java private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); //預編譯(基礎配置) stmt = handler.prepare(connection, transaction.getTimeout()); //設定引數 handler.parameterize(stmt); return stmt; } //statementhandler的方法 public Statement prepare(Connection connection, Integer transactionTimeout) Statement statement = null; //預編譯 statement = instantiateStatement(connection); //設定超時 setStatementTimeout(statement, transactionTimeout); //設定獲取最大行數 setFetchSize(statement); return statement; ``` 還通過`handler.parameterize(stmt);`對引數進行設定,最終通過parameterHandler的setParameters的方法實現了該操作,其中還建立TypeHandler物件完成資料庫型別和javaBean型別的對映。 ```java @Override public void setParameters(PreparedStatement ps) { //。。。省略對value值的操作 //建立TypeHandler物件完成資料庫型別和javaBean型別的對映 TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } //設定引數 typeHandler.setParameter(ps, i + 1, value, jdbcType); } ``` 十一、獲取了ps引數之後,就可以執行statementHandler的query方法進行查詢了 ```java //PreparedStatementHandler.java @Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { //轉為PreparedStatement物件 PreparedStatement ps = (PreparedStatement) statement; ps.execute(); //利用結果集處理物件對結果集進行處理:封裝並返回。 return resultSetHandler. handleResultSets(ps); } ``` 總結: > 反射技術運用廣泛,基於反射的動態代理模式使我們操作的不再是真實的服務,而是代理物件,正是基於動態代理,mybatis可以在真實物件的基礎上,提供額外的服務,我們也可以利用這一特性去自定義一些類,滿足我們的需求。 - 通過動態代理呼叫代理物件的方法。 - 通過sqlSession執行sql操作的方法:insert|delete|select|update - 利用Executor物件對其他三大物件進行排程。 - PreparedStatementHandler對sql進行預編譯,並進行了基礎配置,接著設定引數,並執行sql語句。 - ParameterHandler負責對引數進行設定,其中TypeHandler負責資料庫型別和javabean型別的對映。 - 最後查詢結果由ResultHandler