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


目录
相关文章
|
22天前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
27 10
|
28天前
|
SQL XML Java
Mybatis的原理和MybaitsPlus
这篇文章对比分析了Mybatis和Mybatis Plus的特点与底层实现机制,探讨了两者之间的差异及各自的优势。
44 0
|
3月前
|
SQL Java 数据库连接
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
文章讲述了在使用Mybatis时遇到的资源文件找不到的问题,并提供了通过修改Maven配置来解决资源文件编译到target目录下的方法。
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
3月前
|
XML Java 数据库连接
MyBatis中的接口代理机制及其使用
【8月更文挑战第5天】MyBatis的接口代理机制是其核心功能之一,允许通过定义接口并在运行时生成代理对象来操作数据库。开发者声明一个带有`@Mapper`注解的接口,MyBatis则依据接口方法、映射配置(XML或注解)及数据库信息动态生成代理类。此机制分为四步:创建接口、配置映射文件或使用注解、最后在业务逻辑中注入并使用代理对象。这种方式简化了数据库操作,提高了代码的可读性和可维护性。例如,在电商系统中可通过`OrderMapper`处理订单数据,在社交应用中利用`MessageMapper`管理消息,实现高效且清晰的数据库交互。
|
4月前
|
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); 这使得分页查询变得简单且能获取总记录数等信息。
|
3月前
|
XML Java 数据库连接
Mybatis 模块拆份带来的 Mapper 扫描问题
Mybatis 模块拆份带来的 Mapper 扫描问题
43 0
|
22天前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
68 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
22天前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
39 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
29天前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
209 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个