MyBatis介面繫結原理

LZC發表於2020-07-24
public class App1 {
    public static void main(String[] args) {
        // 載入 MyBatis 配置檔案
        InputStream is = App1.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        // 獲取 SqlSession, 代表和資料庫的一次會話, 用完需要關閉
        // SqlSession 和 Connection, 都是非執行緒安全的, 每次使用都應該去獲取新的物件
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 獲取實現介面的代理物件
        // UserMapper 並沒有實現類, 但是mybatis會為這個介面生成一個代理物件(將介面和xml繫結)
        // 這裡返回的是一個 MapperProxy 代理物件
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        sqlSession.close();
    }
}

從上面可以看出,透過SqlSession來獲取代理類,SqlSession物件表示MyBaits框架與資料庫建立的會話,我們可以透過SqlSession例項完成對資料庫的增刪改查操作。SqlSession是一個介面

// SqlSession.java
public interface SqlSession extends Closeable {
    <T> T selectOne(String statement);
    <T> T selectOne(String statement, Object parameter);
    <E> List<E> selectList(String statement);
    <E> List<E> selectList(String statement, Object parameter);
    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
    <K, V> Map<K, V> selectMap(String statement, String mapKey);
    <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
    <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
    <T> Cursor<T> selectCursor(String statement);
    <T> Cursor<T> selectCursor(String statement, Object parameter);
    <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
    void select(String statement, Object parameter, ResultHandler handler);
    void select(String statement, ResultHandler handler);
    void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
    int insert(String statement);
    int insert(String statement, Object parameter);
    int update(String statement);
    int update(String statement, Object parameter);
    int delete(String statement);
    int delete(String statement, Object parameter);
    void commit();
    void commit(boolean force);
    void rollback();
    void rollback(boolean force);
    List<BatchResult> flushStatements();
    @Override
    void close();
    void clearCache();
    Configuration getConfiguration();
    // 根據介面型別獲取介面對應的代理
    <T> T getMapper(Class<T> type);
    Connection getConnection();
}

SqlSession的預設實現為DefaultSqlSession

// DefaultSqlSession.java
public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;
    private final boolean autoCommit;
    private boolean dirty;
    private List<Cursor<?>> cursorList;
}

@Override
public <T> T getMapper(Class<T> type) {
    // MyBatis初始化時已經把Mapper介面對應的代理資訊儲存到了 Configuration 中
    // 這裡直接從 Configuration 中獲取代理類
    // 這裡返回的是一個 MapperProxy
    // 找到Configuration類的getMapper方法,該方法比較簡單
    // 主要發生如下幾個步驟
    // Map<Class<?>, MapperProxyFactory<?>> knownMappers
    // 1. 從 Configuration 獲取 Mapper 介面對應的 MapperProxyFactory
    // 2. MapperProxyFactory 為 Mapper 介面生成 MapperProxy 代理物件並返回
    return configuration.<T>getMapper(type, this);
}

MyBatis中透過MapperProxy類實現動態代理。下面是MapperProxy類的關鍵程式碼:

// MapperProxy.java
public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    // Mapper 介面
    private final Class<T> mapperInterface;
    // Mapper 介面中的每個方法都會生成一個MapperMethod物件, methodCache維護著他們的對應關係
    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;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是Object中定義的方法,直接執行。如toString(),hashCode()等
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        // 將 Method 轉換成 MapperMethod 並儲存到 methodCache 中
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 這裡面將會執行 Mapper介面對應的 SQL
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            // 透過 Configuration 獲取對應的 MappedStatement
            // 透過 MappedStatement 獲取 MapperMethod 需要的各種資訊
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

MapperProxy使用的是JDK內建的動態代理,實現了InvocationHandler介面,invoke()方法中為通用的攔截邏輯。當我們呼叫自己Mapper介面中的方法時,其實就是在呼叫MapperProxy的invoke()方法。

由上面的分析可以知道,透過MapperMethod來執行代理方法。

public class MapperMethod {
    // 裡面有兩個屬性
    // name: 要執行的方法名,如com.example.demo.UserMapper.getUserByUserName
    // type: SQL標籤的型別 insert update delete select
    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);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        // 根據insert、update、delete、select呼叫不同的方法
        switch (command.getType()) {
            case INSERT: {
                /**
                 * args是使用者 Mapper 所傳遞的方法引數列表 
                 * 如果方法只包含一個引數並且不包含命名引數, 則返回傳遞的引數值。
                 * 如果包含多個引數或包含 @Param 註解修飾的引數,則返回包含名字和對應值的Map物件
                 */
                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 {
                    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());
        }
        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;
    }
}

從上面可以看出,資料的增刪改查都是透過SqlSession來實現的。

MethodSignature

MethodSignatureMapperMethod的內部類。MethodSignatureMapperMethod類提供了三個作用。

  1. 獲取待執行方法中的引數和@Param註解標註的引數名
  2. 獲取標註有@MapKey的值
  3. 獲取SELECT操作時必要的標誌位。
public static class MethodSignature {
    // 是否多值查詢
    private final boolean returnsMany;
    // 是否map查詢
    private final boolean returnsMap;
    // 是否void查詢
    private final boolean returnsVoid;
    // 是否遊標查詢
    private final boolean returnsCursor;
    // 返回型別
    private final Class<?> returnType;
    // 獲取mapKey的值
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    // 引數解析器
    private final ParamNameResolver paramNameResolver;

    /**
     * args是使用者 Mapper 所傳遞的方法引數列表 
     * 如果方法只包含一個引數並且不包含命名引數, 則返回傳遞的引數值。
     * 如果包含多個引數或包含命名引數,則返回包含名字和對應值的Map物件
     */
    public Object convertArgsToSqlCommandParam(Object[] args) {
        return paramNameResolver.getNamedParams(args);
    }
}

ParamNameResolver

public class ParamNameResolver {

    private static final String GENERIC_NAME_PREFIX = "param";

    // 儲存方法引數列表的引數名
    // 例如方法:getUser(String userName, Integer age)
    // names.get(0) = arg0,names.get(1) = arg1

    // 例如方法:getUser(@Param(value = "userName") String userName, Integer age)
    // names.get(0) = 'userName',names.get(1) = arg1

    // key儲存引數所在位置的下標
    // value為引數名,如果存在@Param,則使用@Param註解裡面的值,否則 value = 'arg' + 引數下標索引值
    private final SortedMap<Integer, String> names;

    // 方法引數列表是否存在 @Param 註解的引數
    private boolean hasParamAnnotation;

    /**
     * args是使用者 Mapper 所傳遞的方法引數列表 
     * 如果方法只包含一個引數並且不包含命名引數, 則返回傳遞的引數值。
     * 如果包含多個引數或包含 @Param 註解修飾的引數,則返回包含名字和對應值的Map物件
     */
    public Object getNamedParams(Object[] args) {
        final int paramCount = names.size();
        if (args == null || paramCount == 0) {
            // 如果方法引數為空,則直接放回 null 
            return null;
        } else if (!hasParamAnnotation && paramCount == 1) {
            // 如果方法引數沒有使用 @Param 註解修飾的引數,並且只有一個引數
            // 這裡直接返回引數值,相當於是返回args[0]
            return args[names.firstKey()];
        } else {
            // 建立一個Map物件
            // key 為引數名,value為引數值
            final Map<String, Object> param = new ParamMap<Object>();
            int i = 0;
            for (Map.Entry<Integer, String> entry : names.entrySet()) {

                param.put(entry.getValue(), args[entry.getKey()]);

                // add generic param names (param1, param2, ...)
                final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
                // ensure not to overwrite parameter named with @Param
                if (!names.containsValue(genericParamName)) {
                    param.put(genericParamName, args[entry.getKey()]);
                }
                i++;
            }
            return param;
        }
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章