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 方法并返回结果。


目录
相关文章
|
14天前
|
XML Java 数据库连接
MyBatis中的接口代理机制及其使用
【8月更文挑战第5天】MyBatis的接口代理机制是其核心功能之一,允许通过定义接口并在运行时生成代理对象来操作数据库。开发者声明一个带有`@Mapper`注解的接口,MyBatis则依据接口方法、映射配置(XML或注解)及数据库信息动态生成代理类。此机制分为四步:创建接口、配置映射文件或使用注解、最后在业务逻辑中注入并使用代理对象。这种方式简化了数据库操作,提高了代码的可读性和可维护性。例如,在电商系统中可通过`OrderMapper`处理订单数据,在社交应用中利用`MessageMapper`管理消息,实现高效且清晰的数据库交互。
|
23天前
|
SQL Java 数据库连接
springboot~mybatis-pagehelper原理与使用
【7月更文挑战第15天】MyBatis-PageHelper是用于MyBatis的分页插件,基于MyBatis的拦截器机制实现。它通过在SQL执行前动态修改SQL语句添加LIMIT子句以支持分页。使用时需在`pom.xml`添加依赖并配置方言等参数。示例代码: PageHelper.startPage(2, 10); List&lt;User&gt; users = userMapper.getAllUsers(); PageInfo&lt;User&gt; pageInfo = new PageInfo&lt;&gt;(users); 这使得分页查询变得简单且能获取总记录数等信息。
|
8天前
|
XML Java 数据库连接
Mybatis 模块拆份带来的 Mapper 扫描问题
Mybatis 模块拆份带来的 Mapper 扫描问题
15 0
|
1月前
|
Java 数据库连接 Maven
文本,使用SpringBoot工程创建一个Mybatis-plus项目,Mybatis-plus在编写数据层接口,用extends BaseMapper<User>继承实体类
文本,使用SpringBoot工程创建一个Mybatis-plus项目,Mybatis-plus在编写数据层接口,用extends BaseMapper<User>继承实体类
|
1月前
|
SQL
自定义SQL,可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,如何自定义SQL呢?利用MyBatisPlus的Wrapper来构建Wh,在mapper方法参数中用Param注
自定义SQL,可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,如何自定义SQL呢?利用MyBatisPlus的Wrapper来构建Wh,在mapper方法参数中用Param注
|
1月前
|
SQL Java 数据库连接
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
31 0
若依修改,集成mybatisplus报错,若依集成mybatisplus,总是找不到映射是怎么回事只要是用mp的方法就找报,改成mybatisPlus配置一定要改
若依修改,集成mybatisplus报错,若依集成mybatisplus,总是找不到映射是怎么回事只要是用mp的方法就找报,改成mybatisPlus配置一定要改
MybatisPlus--IService接口基本用法,MP提供了Service接口,save(T) 这里的意思是新增了一个T, saveBatch 是批量新增的意思,saveOrUpdate是增或改
MybatisPlus--IService接口基本用法,MP提供了Service接口,save(T) 这里的意思是新增了一个T, saveBatch 是批量新增的意思,saveOrUpdate是增或改
|
1月前
|
XML Java 数据格式
支付系统----微信支付20---创建案例项目--集成Mybatis-plus的补充,target下只有接口的编译文件,xml文件了,添加日志的写法
支付系统----微信支付20---创建案例项目--集成Mybatis-plus的补充,target下只有接口的编译文件,xml文件了,添加日志的写法
接口模板,文本常用的接口Controller层,常用的controller层模板,Mybatisplus的相关配置
接口模板,文本常用的接口Controller层,常用的controller层模板,Mybatisplus的相关配置