MyBatis Mapper 接口方法执行原理分析

简介: 前言通过前面入门 MyBatis 的文章《MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)》,我们已经对 MyBatis 有了一定了解。

前言


通过前面入门 MyBatis 的文章《MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)》,我们已经对 MyBatis 有了一定了解。MyBatis 的 Mapper 有两种形式,第一种是 xml 文件,用来配置映射关系及 SQL,第二种是 Java 接口。通常来说,我们倾向于在 xml 中创建 Java 接口方法对应的查询语句,通过调用 Mapper 接口方法来操作数据库。使用 Mapper 接口方法的形式替代了调用 SqlSession 的方法,避免了字符串拼写错误的问题,那么 Mapper 接口是如何实例化的呢?Mapper 接口方法又是如何执行的?本篇将进行分析。


Mapper 接口实例化分析


Mapper 接口方法的执行需要先获取 Mapper 接口的实例,我们都知道,Java 中的接口是不能实例化的,先看如何通过 SqlSession 获取 Mapper 接口实例的。SqlSession 默认的实现是 DefaultSqlSession,跟踪#getMapper方法。


public class DefaultSqlSession implements SqlSession {
  // MyBatis 配置
    private final Configuration configuration;
    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }
}


我们看到 DefaultSqlSession 把获取 Mapper 的工作委托给了 Configuration,继续跟踪源码。


public class Configuration {
  // Mapper 注册中心
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    // 获取 Mapper 实例
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
}


Configuration 和 DefaultSqlSession 一样又偷懒了,委托给了 Mapper 的注册中心 MapperRegistry 获取 Mapper,再跟踪源码。


public class MapperRegistry {
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  // 获取 Mapper 实例
    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);
        }
    }
}


这里终于看到具体的实现了,MapperRegistry 先根据 Mapper 类型取出缓存的 MapperProxyFactory,然后调用 MapperProxyFactory 的方法创建 Mapper 接口的实例。那么 MapperProxyFactory 的缓存什么时候存进去的呢? 查看存放缓存的 knownMappers 在哪使用,我们可以发现如下的代码。


public class 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 {
                knownMappers.put(type, new MapperProxyFactory<>(type));
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
}


原来是在 MapperRegistry 添加 Mapper 时使用的,继续跟踪代码。


public class Configuration {
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
}


我们发现是在配置中添加的 Mapper 接口,继续跟踪的话则会发现是在解析 Mapper xml 文件时添加的。整个获取 MapperProxyFactory 的流程和添加的方向是刚好相反的。总结 MapperRegistry 中的 MapperProxyFactory 添加流程如下:Mapper xml 文件解析 -> 添加 Mapper 接口到 Configuration -> 缓存 MapperProxyFactory 到 MapperRegistry。


分析到这里,我们就可以继续跟踪MapperProxyFactory#newInstance(SqlSession)方法了。


public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
    // 获取 Mapper 接口的实例
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }    
}


我们发现 MapperProxyFactory 实例化出一个 MapperProxy 的实例,然后使用 JDK 动态代理创建了 Mapper 接口的实例,至此,Mapper 接口的实例化就比较清晰了。关于代理,不熟悉的小伙伴可参考文章《Java 中创建代理的几种方式》。


Mapper 接口方法执行分析


通过上面的内容我们知道MyBatis 最终会使用 JDK 动态代理创建出 Mapper 接口的代理实例,并且使用 MapperProxy 处理方法的调用。那么我们就跟踪 MapperProxy 的源码。


public class MapperProxy<T> implements InvocationHandler, Serializable {
    @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 {
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }
}


MapperProxy 是一个 InvocationHandler,当调用接口的方法时会委托给InvocationHandler#invoke方法,该方法又调用了MapperProxy#cachedInvoker方法返回值的#invoke方法,查看#cachedInvoker方法如下。


public class MapperProxy<T> implements InvocationHandler, Serializable {
    // 获取 Mapper 方法调用器
    private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
        try {
            // 优先从缓存获取
            MapperMethodInvoker invoker = methodCache.get(method);
            if (invoker != null) {
                return invoker;
            }
            return methodCache.computeIfAbsent(method, m -> {
                if (m.isDefault()) {
                    ... 省略处理默认接口方法的 MapperMethodInvoker
                } else {
          // 普通接口方法的调用器
                    return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
                }
            });
        } catch (RuntimeException re) {
            Throwable cause = re.getCause();
            throw cause == null ? re : cause;
        }
    }
}


#cachedInvoker方法返回了 MapperMethodInvoker 对象,所以 Mapper 方法的调用由MapperMethodInvoker#invoke方法处理,对于普通的接口方法,#cachedInvoker方法返回的是一个通过 MapperMethod 实例化的 PlainMethodInvoker,跟踪PlainMethodInvoker#invoke方法如下。


public class MapperProxy<T> implements InvocationHandler, Serializable {
  // 普通的接口方法调用器
    private static class PlainMethodInvoker implements MapperMethodInvoker {
        private final MapperMethod mapperMethod;
        public PlainMethodInvoker(MapperMethod mapperMethod) {
            super();
            this.mapperMethod = mapperMethod;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          // mapper 方法调用执行逻辑
            return mapperMethod.execute(sqlSession, args);
        }
    }
}


PlainMethodInvoker 持有表示 Mapper 方法的 MapperMethod,#invoke方法委托MapperMethod#execute处理,继续跟踪源码。


public class MapperMethod {
    // 使用的 SQL
    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);
    }
  // Mapper 方法执行
    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()) {
                    // 方法返回类型为 void
                    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);
                    if (method.returnsOptional()
                        && (result == null || !method.getReturnType().equals(result.getClass()))) {
                        result = Optional.ofNullable(result);
                    }
                }
                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;
    }
}


MapperMethod 表示是某一个 Mapper 接口方法的抽象,在实例化时会根据接口及配置信息确定 Mapper xml 文件中要执行的 SQL ,根据执行 SQL 的类型,使用不同的逻辑处理,对于 insert、update、delete 三种类型的逻辑处理方式基本一致,select 语句的处理则相对复杂,当 Mapper 方法的返回值是数组或者集合时会将MapperMethod#executeForMany的结果作为 Mapper 方法的返回值,跟踪该方法如下。


public class MapperMethod {
  // 列表查询
    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectList(command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectList(command.getName(), param);
        }
        // issue #510 Collections & arrays support
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
            if (method.getReturnType().isArray()) {
                return convertToArray(result);
            } else {
                return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
            }
        }
        return result;
    }
}


这里又回到了 SqlSession,也就是说 Mapper 方法的执行最终还是调用 SqlSession 的方法,这和没有 Mapper 接口时的行为是一致的,SqlSession 内部的流程由于比较复杂后面会再分析,到了这里我们终于可以对 Mapper 方法的整个执行流程做一个总结。


总结

上面主要以跟随源码的方式进行分析 Mapper 方法执行原理,其整个流程较长,只看源码的话难免陷入细节,这里就对整个流程做一个总结。


SqlSessionFactoryBuilder 构建 SqlSessionFactory 时解析 Mapper xml 文件。

1.1. 添加 Mapper xml 文件对应的接口到 Configuration。

1.2. 向 Configuration 中的 MapperRegistry 注册 Mapper 接口。

1.3 .MapperRegistry 生成 Mapper 接口的代理工厂 MapperProxyFactory。

SqlSessionFactory 获取 SqlSession。

SqlSession 获取 Mapper 接口实例。

3.1. 从 Configuration 中获取 Mapper 实例。

3.2. Configuration 从 MapperRegistry 获取 Mapper 实例。

3.3. MapperRegistry 使用 MapperProxyFactory 创建 Mapper 接口的代理对象。

Mapper 接口非默认方法执行。

4.1. Mapper 接口代理对象执行MapperProxy#invoke方法。

4.2. MapperProxy#invoke方法内调用PlainMethodInvoker#invoke方法。

4.3. PlainMethodInvoker#invoke方法内调用MapperMethod#execute。

4.4. MapperMethod 调用对应的 SqlSession 方法并返回结果。


目录
相关文章
|
6天前
|
SQL XML Java
一文搞懂Mybatis执行原理
一文搞懂Mybatis执行原理
24 1
|
8天前
|
Java 数据库连接 Maven
使用mybatis插件generator生成实体类,dao层和mapper映射
使用mybatis插件generator生成实体类,dao层和mapper映射
19 0
|
2月前
|
Java 数据库连接 mybatis
mybatis 框架分析——mybatis框架使用篇
mybatis 框架分析——mybatis框架使用篇
23 0
|
2月前
|
SQL Java 数据库连接
mybatis常见分页技术和自定义分页原理实战
mybatis常见分页技术和自定义分页原理实战
|
1月前
|
SQL Java 数据库连接
Mybatis之Mybatis简介、搭建Mybatis相关步骤(开发环境、maven、核心配置文件、mapper接口、映射文件、junit测试、log4j日志)
【1月更文挑战第2天】 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下,iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
143 3
Mybatis之Mybatis简介、搭建Mybatis相关步骤(开发环境、maven、核心配置文件、mapper接口、映射文件、junit测试、log4j日志)
|
7天前
|
Java 数据库连接 mybatis
结构性模式之---外观模式、MyBatis中Configuration的外观模式的使用分析
结构性模式之---外观模式、MyBatis中Configuration的外观模式的使用分析
25 1
|
1月前
|
测试技术 数据库
深入探索MyBatis-Plus中Service接口的lambdaUpdate用法及示例
深入探索MyBatis-Plus中Service接口的lambdaUpdate用法及示例
51 0
|
1月前
|
SQL XML Java
关于mybatis-plus写自定义方法(自定义sql)
关于mybatis-plus写自定义方法(自定义sql)
34 1
|
1月前
|
SQL 缓存 Java
mybatis工作原理
mybatis工作原理
77 0