Mybatis SqlSession執行過程原始碼分析
簡述
- 在我們使用Mybatis進行增刪改查時,SqlSession是核心,它相當於一個數據庫連線物件,在一個SqlSession中可以執行多條SQL語句
- SqlSession本身是一個介面,提供了很多種操作方法,如insert,select等等,我們可以直接呼叫,但是這種方式是不推薦的,可讀性,可維護性都不是很高,推薦使用Mapper介面對映的方式去進行增刪改查,瞭解一下這種方式的執行過程也是有必要的
- 在瞭解SqlSession執行過程前,我們需要具備動態代理以及JDK動態代理的相關知識
開始分析
我們在使用Mapper時,基本流程就是在配置檔案中指定好Mapper介面檔案和XML檔案的路徑,然後使用如下程式碼獲取代理物件
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
然後呼叫介面中的方法即可調執行相關的SQL語句,這一過程是具體怎麼執行的呢?下面開始分析
【getMapper方法】
- SqlSession只是一個介面,Mybatis中使用的是它的預設實現類DefaultSqlSession
@Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }
- 呼叫Configuration的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
- 繼續呼叫MapperRegistry的getMapper方法
@SuppressWarnings("unchecked") 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); } }
這裡需要介紹一下,我們在寫XML配置檔案時註冊了Mapper,被註冊的Mapper就被維護在MapperRegistry的一個HashMap中
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
map的key是Class類,value是對應的一個代理工廠MapperProxyFactory,用來生產對應的代理類,如果key沒有對應的value,會丟擲異常,告訴我們並沒有去註冊這個Mapper,這時候就需要去檢查配置檔案了
- 呼叫MapperProxyFactory的newInstance()方法去生產代理物件
public T newInstance(SqlSession sqlSession) { //生產代理,該物件必須實現InvocationHandler介面且實現invoke方法 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
這裡就涉及到了JDK動態代理
- 首先new一個Mapper代理,由JDK動態代理可知,代理物件必須實現InvocationHandler介面且實現invoke方法
- 呼叫Proxy.newProxyInstance方法產生代理物件
為什麼要動態代理呢?因為我們要執行SQL!在介面方法中,我們是不需要自己寫查詢方法的,而SQL方法的執行就依賴於動態代理!
- 動態代理的主要邏輯就是invoke方法中的內容,我們來看MapperProxy的invoke方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
簡單描述一些流程
- 我們知道Java中,所有類的父類都是Object,因此所有類都會繼承一些Object的方法,如toString()之類的,所以這裡我們要進行判斷,如果我們的代理物件呼叫的是從Object那裡繼承來的方法,我們就不去執行動態代理邏輯,而是直接執行該方法
- 如果執行的方法歸本類所有,則通過cachedMapperMethod產生MapperMethod來執行execute代理邏輯,即執行SQL
【這裡涉及到了一個很重要的問題!!!】
我們知道,XML對映檔案其實就是Mapper介面的實現類,介面方法的全路徑和XML對映檔案中namesapce + SQL塊的id一一對應,Mybatis是如何找到並維護這種對應關係的呢?就是通過MapperProxy中的一個Map
private final Map<Method, MapperMethod> methodCache;
這個Map的key為傳入的方法型別,value為MapperMethod,也是執行SQL的關鍵,其實這個methodCache主要是為了完成快取的任務,因為我們執行方法時,需要從Configuration中去解析對應的XML檔案中的SQL塊,多次解析會造成資源的浪費,程式碼如下
private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }
若key對應的value存在,則從map直接拿到並返回,若不存在,則通過
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
解析後,放入map,並返回,下面看是如何解析的
private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); }
在MapperMethod中有兩個成員變數command和method,command主要完成了根據method方法解析出對應的SQL塊,而method負責一些如傳參的轉化,返回型別的判定等,這裡我們主要看command解析(註釋中)
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { //1、拿到介面全路徑.方法名稱的字串,這不就是XML對映檔案中的namespace.id嗎? String statementName = mapperInterface.getName() + "." + method.getName(); MappedStatement ms = null; if (configuration.hasStatement(statementName)) { //2、判斷Configuration中是否含有該配置,有則拿到MappedStatement ms = configuration.getMappedStatement(statementName); } else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35 //3、如果不存在,看看它的父類全路徑.方法名稱是否存在 String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); if (configuration.hasStatement(parentStatementName)) { ms = configuration.getMappedStatement(parentStatementName); } } if (ms == null) { //如果MappedStatement為空 if(method.getAnnotation(Flush.class) != null){ //先瞅瞅執行的這個方法有木有@Flush註解呢?有就執行下面操作 name = null; type = SqlCommandType.FLUSH; } else { //沒有。。。那就拋異常唄,趕緊去檢查是不是XML和介面對應寫錯了? throw new BindingException("Invalid bound statement (not found): " + statementName); } } else { //不等於空就執行下面操作 name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } }
至此根據method解析SQL塊完畢,這一系列初始化過程在new這個MapperMethod物件後便完成了,然後便呼叫execute方法進行執行代理方法,即SQL執行!
public Object execute(SqlSession sqlSession, Object[] args) { Object result; if (SqlCommandType.INSERT == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { 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 { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else if (SqlCommandType.FLUSH == command.getType()) { result = sqlSession.flushStatements(); } else { throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.ge tName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
這一段程式碼就不去一一解析了,主要就是通過初始化好的command,拿到SQL塊的id以及型別,method拿到SQL引數以及返回型別,通過呼叫sqlSession的原生方法如select、selectOne、insert、update等等去執行SQL
總結
哇,終於寫完了,這次的原始碼分析真的好爽,一步步瞭解了Mybatis的SqlSession是如何getMapper並呼叫方法的,如果你能看完,真的十分感謝!
(這裡最後只到了呼叫SqlSession原生的增刪改查方法,而並沒有去深入這些方法是如何實現的,大家有興趣可以自行去檢視原始碼,有機會下次再去分析!)