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

系列文章目录

这是 MyBatis 源码之旅的第二篇文章,MyBatis 版本号为 3.5.6,源码分析注释已上传到 Github ,第一篇的文章目录如下,建议按照顺序阅读。
  1. 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 方法执行原理,其整个流程较长,只看源码的话难免陷入细节,这里就对整个流程做一个总结。

  1. SqlSessionFactoryBuilder 构建 SqlSessionFactory 时解析 Mapper xml 文件。
    1.1. 添加 Mapper xml 文件对应的接口到 Configuration。
    1.2. 向 Configuration 中的 MapperRegistry 注册 Mapper 接口。
    1.3 .MapperRegistry 生成 Mapper 接口的代理工厂 MapperProxyFactory。
  2. SqlSessionFactory 获取 SqlSession。
  3. SqlSession 获取 Mapper 接口实例。
    3.1. 从 Configuration 中获取 Mapper 实例。
    3.2. Configuration 从 MapperRegistry 获取 Mapper 实例。
    3.3. MapperRegistry 使用 MapperProxyFactory 创建 Mapper 接口的代理对象。
  4. Mapper 接口非默认方法执行。
    4.1. Mapper 接口代理对象执行MapperProxy#invoke方法。
    4.2. MapperProxy#invoke方法内调用PlainMethodInvoker#invoke方法。
    4.3. PlainMethodInvoker#invoke方法内调用MapperMethod#execute
    4.4. MapperMethod 调用对应的 SqlSession 方法并返回结果。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
MyBatis是一个Java持久层框架,提供了灵活的SQL映射和便捷的数据库访问。Mapper接口MyBatis中定义SQL映射的方式之一。下面是MyBatis Mapper源码分析的一般步骤: 1. 首先,需要了解Mapper接口和XML映射文件之间的对应关系。在XML映射文件中定义了SQL语句和结果映射规则,而Mapper接口则提供了对应的方法。 2. 接下来,可以分析Mapper接口的实现类。MyBatis会动态生成Mapper接口的实现类,这些实现类会根据XML映射文件中定义的SQL语句进行具体的数据库操作。 3. 在实现类中,可以找到具体的数据库操作方法。这些方法会通过SqlSession对象执行对应的SQL语句,并返回结果。 4. 在执行SQL语句之前,MyBatis会进行参数解析和类型转换等操作。可以分析这部分代码,了解参数处理的过程。 5. SQL语句的执行过程中,还涉及到一些与数据库连接相关的操作,比如连接的获取和释放。可以查看这部分代码,了解连接管理的实现方式。 6. 最后,可以分析SQL语句的执行结果处理过程。MyBatis会根据XML映射文件中定义的结果映射规则,将查询结果转换成相应的Java对象。 需要注意的是,MyBatis源码比较庞大复杂,以上只是对Mapper源码分析的一般步骤。具体的分析过程可能会因版本和具体使用情况而有所不同。建议先阅读MyBatis的官方文档和相关资料,对其基本原理有所了解,再进行源码分析
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值