Mybaits 源碼解析 (五)----- Mapper接口底層原理(爲何Mapper不用寫實現類就能訪問到數據庫?)

剛開始使用Mybaits的同窗有沒有這樣的疑惑,爲何咱們沒有編寫Mapper的實現類,卻能調用Mapper的方法呢?本篇文章我帶你們一塊兒來解決這個疑問sql

上一篇文章咱們獲取到了DefaultSqlSession,接着咱們來看第一篇文章測試用例後面的代碼數據庫

EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class);
List<Employee> allEmployees = employeeMapper.getAll();

爲 Mapper 接口建立代理對象

咱們先從 DefaultSqlSession 的 getMapper 方法開始看起,以下:數組

 1 public <T> T getMapper(Class<T> type) {
 2     return configuration.<T>getMapper(type, this);
 3 }
 4 
 5 // Configuration
 6 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
 7     return mapperRegistry.getMapper(type, sqlSession);
 8 }
 9 
10 // MapperRegistry
11 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
12     // 從 knownMappers 中獲取與 type 對應的 MapperProxyFactory
13     final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 14     if (mapperProxyFactory == null) {
15         throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
16     }
17     try {
18         // 建立代理對象
19         return mapperProxyFactory.newInstance(sqlSession); 20     } catch (Exception e) {
21         throw new BindingException("Error getting mapper instance. Cause: " + e, e);
22     }
23 }

這裏最重要就是兩行代碼,第13行和第19行,咱們接下來就分析這兩行代碼緩存

獲取MapperProxyFactory

根據名稱看,能夠理解爲Mapper代理的建立工廠,是否是Mapper的代理對象由它建立呢?咱們先來回顧一下knownMappers 集合中的元素是什麼時候存入的。這要在我前面的文章中找答案,MyBatis 在解析配置文件的 <mappers> 節點的過程當中,會調用 MapperRegistry 的 addMapper 方法將 Class 到 MapperProxyFactory 對象的映射關係存入到 knownMappers。有興趣的同窗能夠看看我以前的文章,咱們來回顧一下源碼:app

private void bindMapperForNamespace() {
    // 獲取映射文件的命名空間
    String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 根據命名空間解析 mapper 類型
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
        }
        if (boundType != null) {
            // 檢測當前 mapper 類是否被綁定過
            if (!configuration.hasMapper(boundType)) {
                configuration.addLoadedResource("namespace:" + namespace);
                // 綁定 mapper 類
                configuration.addMapper(boundType);
            }
        }
    }
}

// Configuration
public <T> void addMapper(Class<T> type) {
    // 經過 MapperRegistry 綁定 mapper 類
    mapperRegistry.addMapper(type);
}

// MapperRegistry
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 {
            /*
             * 將 type 和 MapperProxyFactory 進行綁定,MapperProxyFactory 可爲 mapper 接口生成代理類
             */ knownMappers.put(type, new MapperProxyFactory<T>(type));
            
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // 解析註解中的信息
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

在解析Mapper.xml的最後階段,獲取到Mapper.xml的namespace,而後利用反射,獲取到namespace的Class,並建立一個MapperProxyFactory的實例,namespace的Class做爲參數,最後將namespace的Class爲key,MapperProxyFactory的實例爲value存入knownMappers。函數

注意,咱們這裏是經過映射文件的命名空間的Class當作knownMappers的Key。而後咱們看看getMapper方法的13行,是經過參數Employee.class也就是Mapper接口的Class來獲取MapperProxyFactory,因此咱們明白了爲何要求xml配置中的namespace要和和對應的Mapper接口的全限定名了測試

生成代理對象

咱們看第19行代碼 return mapperProxyFactory.newInstance(sqlSession);,很明顯是調用了MapperProxyFactory的一個工廠方法,咱們跟進去看看ui

public class MapperProxyFactory<T> {
    //存放Mapper接口Class
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return this.mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return this.methodCache;
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
        //生成mapperInterface的代理類
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
         /*
         * 建立 MapperProxy 對象,MapperProxy 實現了 InvocationHandler 接口,代理邏輯封裝在此類中
         * 將sqlSession傳入MapperProxy對象中,第二個參數是Mapper的接口,並非其實現類
         */
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

上面的代碼首先建立了一個 MapperProxy 對象,該對象實現了 InvocationHandler 接口。而後將對象做爲參數傳給重載方法,並在重載方法中調用 JDK 動態代理接口爲 Mapper接口 生成代理對象。this

這裏要注意一點,MapperProxy這個InvocationHandler 建立的時候,傳入的參數並非Mapper接口的實現類,咱們之前是怎麼建立JDK動態代理的?先建立一個接口,而後再建立一個接口的實現類,最後建立一個InvocationHandler並將實現類傳入其中做爲目標類,建立接口的代理類,而後調用代理類方法時會回調InvocationHandler的invoke方法,最後在invoke方法中調用目標類的方法,可是咱們這裏調用Mapper接口代理類的方法時,須要調用其實現類的方法嗎?不須要,咱們須要調用對應的配置文件的SQL,因此這裏並不須要傳入Mapper的實現類到MapperProxy中,那Mapper接口的代理對象是如何調用對應配置文件的SQL呢?下面咱們來看看。spa

Mapper代理類如何執行SQL?

上面一節中咱們已經獲取到了EmployeeMapper的代理類,而且其InvocationHandler爲MapperProxy,那咱們接着看Mapper接口方法的調用

List<Employee> allEmployees = employeeMapper.getAll();

知道JDK動態代理的同窗都知道,調用代理類的方法,最後都會回調到InvocationHandler的Invoke方法,那咱們來看看這個InvocationHandler(MapperProxy)

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 若是方法是定義在 Object 類中的,則直接調用
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable var5) {
                throw ExceptionUtil.unwrapThrowable(var5);
            }
        } else {
            // 從緩存中獲取 MapperMethod 對象,若緩存未命中,則建立 MapperMethod 對象
            MapperMethod mapperMethod = this.cachedMapperMethod(method); // 調用 execute 方法執行 SQL
            return mapperMethod.execute(this.sqlSession, args);
        }
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
        if (mapperMethod == null) {
            //建立一個MapperMethod,參數爲mapperInterface和method還有Configuration
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            this.methodCache.put(method, mapperMethod);
        }

        return mapperMethod;
    }
}

如上,回調函數invoke邏輯會首先檢測被攔截的方法是否是定義在 Object 中的,好比 equals、hashCode 方法等。對於這類方法,直接執行便可。緊接着從緩存中獲取或者建立 MapperMethod 對象,而後經過該對象中的 execute 方法執行 SQL。咱們先來看看如何建立MapperMethod

建立 MapperMethod 對象

public class MapperMethod {

    //包含SQL相關信息,比喻MappedStatement的id屬性,(mapper.EmployeeMapper.getAll)
    private final SqlCommand command;
    //包含了關於執行的Mapper方法的參數類型和返回類型。
    private final MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        // 建立 SqlCommand 對象,該對象包含一些和 SQL 相關的信息
        this.command = new SqlCommand(config, mapperInterface, method);
        // 建立 MethodSignature 對象,從類名中可知,該對象包含了被攔截方法的一些信息
        this.method = new MethodSignature(config, mapperInterface, method);
    }
}

MapperMethod包含SqlCommand 和MethodSignature 對象,咱們來看看其建立過程

① 建立 SqlCommand 對象

public static class SqlCommand {
    //name爲MappedStatement的id,也就是namespace.methodName(mapper.EmployeeMapper.getAll)
    private final String name; //SQL的類型,如insert,delete,update
    private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
        //拼接Mapper接口名和方法名,(mapper.EmployeeMapper.getAll)
        String statementName = mapperInterface.getName() + "." + method.getName(); MappedStatement ms = null; //檢測configuration是否有key爲mapper.EmployeeMapper.getAll的MappedStatement
        if (configuration.hasStatement(statementName)) { //獲取MappedStatement
            ms = configuration.getMappedStatement(statementName);
        } else if (!mapperInterface.equals(method.getDeclaringClass())) {
            String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
            if (configuration.hasStatement(parentStatementName)) {
                ms = configuration.getMappedStatement(parentStatementName);
            }
        }
        
        // 檢測當前方法是否有對應的 MappedStatement
        if (ms == null) {
            if (method.getAnnotation(Flush.class) != null) {
                name = null;
                type = SqlCommandType.FLUSH;
            } else {
                throw new BindingException("Invalid bound statement (not found): "
                    + mapperInterface.getName() + "." + methodName);
            }
        } else {
            // 設置 name 和 type 變量
            name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) {
                throw new BindingException("Unknown execution method for: " + name);
            }
        }
    }
}

public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
    //檢測configuration是否有key爲statementName的MappedStatement
    return this.mappedStatements.containsKey(statementName);
}

經過拼接接口名和方法名,在configuration獲取對應的MappedStatement,並設置設置 name 和 type 變量,代碼很簡單

② 建立 MethodSignature 對象

MethodSignature 包含了被攔截方法的一些信息,如目標方法的返回類型,目標方法的參數列表信息等。下面,咱們來看一下 MethodSignature 的構造方法。

public static class MethodSignature {

    private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {

        // 經過反射解析方法返回類型
        Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
        if (resolvedReturnType instanceof Class<?>) {
            this.returnType = (Class<?>) resolvedReturnType;
        } else if (resolvedReturnType instanceof ParameterizedType) {
            this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
        } else {
            this.returnType = method.getReturnType();
        }
        
        // 檢測返回值類型是不是 void、集合或數組、Cursor、Map 等
        this.returnsVoid = void.class.equals(this.returnType);
        this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
        this.returnsCursor = Cursor.class.equals(this.returnType);
        // 解析 @MapKey 註解,獲取註解內容
        this.mapKey = getMapKey(method);
        this.returnsMap = this.mapKey != null;
        /*
         * 獲取 RowBounds 參數在參數列表中的位置,若是參數列表中
         * 包含多個 RowBounds 參數,此方法會拋出異常
         */ 
        this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
        // 獲取 ResultHandler 參數在參數列表中的位置
        this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
        // 解析參數列表
        this.paramNameResolver = new ParamNameResolver(configuration, method);
    }
}

執行 execute 方法

前面已經分析了 MapperMethod 的初始化過程,如今 MapperMethod 建立好了。那麼,接下來要作的事情是調用 MapperMethod 的 execute 方法,執行 SQL。傳遞參數sqlSession和method的運行參數args

return mapperMethod.execute(this.sqlSession, args);

咱們去MapperMethod 的execute方法中看看

MapperMethod

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    
    // 根據 SQL 類型執行相應的數據庫操做
    switch (command.getType()) {
        case INSERT: {
            // 對用戶傳入的參數進行轉換,下同
            Object param = method.convertArgsToSqlCommandParam(args); // 執行插入操做,rowCountResult 方法用於處理返回值
            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()) {
                // 執行查詢操做,並將結果封裝在 Map 中返回
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                // 執行查詢操做,並返回一個 Cursor 對象
                result = executeForCursor(sqlSession, args);
            } else {
                Object param = method.convertArgsToSqlCommandParam(args);
                // 執行查詢操做,並返回一個結果
                result = sqlSession.selectOne(command.getName(), param);
            }
            break;
        case FLUSH:
            // 執行刷新操做
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
}

如上,execute 方法主要由一個 switch 語句組成,用於根據 SQL 類型執行相應的數據庫操做。咱們先來看看是參數的處理方法convertArgsToSqlCommandParam是如何將方法參數數組轉化成Map的

public Object convertArgsToSqlCommandParam(Object[] args) {
    return paramNameResolver.getNamedParams(args);
}

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
    } else {
        //建立一個Map,key爲method的參數名,值爲method的運行時參數值
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // 添加 <參數名, 參數值> 鍵值對到 param 中
            param.put(entry.getValue(), args[entry.getKey()]);
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

咱們看到,將Object[] args轉化成了一個Map<參數名, 參數值> ,接着咱們就能夠看查詢過程分析了,以下

// 執行查詢操做,並返回一個結果
result = sqlSession.selectOne(command.getName(), param);

咱們看到是經過sqlSession來執行查詢的,而且傳入的參數爲command.getName()和param,也就是namespace.methodName(mapper.EmployeeMapper.getAll)和方法的運行參數。

查詢操做咱們下一篇文章單獨來說

相關文章
相關標籤/搜索