Java-17 深入浅出MyBatis Mapper Proxy 源码解析:从 getMapper 到 invoke 的完整链路

TL;DR

  • 场景 :Java 后端开发者在阅读 MyBatis 源码时,对 Mapper 接口如何被"无实现"地转换成 SQL 调用感到困惑;只知道 sqlSession.getMapper(XxxMapper.class) 拿到的对象能直接调方法,但不清楚背后是 JDK 动态代理 + MapperProxy + MapperMethod 三层结构在协作。

  • 结论 :MyBatis 通过 MapperRegistry 维护接口→MapperProxyFactory 的映射;getMapper 触发 MapperProxyFactory.newInstance 用 JDK 动态代理生成实例;方法调用被 MapperProxy.invoke 拦截,缓存到 MapperMethodexecute 派发到 SqlSession.insert/update/delete/selectOne/selectList/...;理解这条链路就能解释为何 Mapper 接口不需要实现类、为何能切换 XML/注解、为何插件(Interceptor)能在执行前后切入。

  • 产出getMapper → MapperProxyFactory → MapperProxy → MapperMethod → SqlSession 完整调用链 + 逐行源码解读 + 关键判断分支(Object 方法、default method、返回值类型 Many/Map/Cursor/Optional/primitive)+ 8 条典型错误速查(BindingException、namespace 不匹配、原始类型返回 null、Mapper 未扫描、CGLIB 误用、default 方法拦截、插件链断裂、参数转换失败)。

源码解析

续接上节

基本概念

MyBatis 使用 Mapper Proxy 动态代理实现对数据库操作的封装,用户只需定义一个接口(Mapper 接口),在接口方法上通过注解或 XML 文件指定 SQL 语句的执行逻辑,而不需要手动实现接口的方法。

  • Mapper 接口:定义了数据库操作方法的接口,例如 insertUser, getUserById 等。
  • 动态代理:在运行时生成接口实现类的实例,将方法调用映射为对应的 SQL 操作。

MyBatis 的 Mapper Proxy 是一个实现了数据访问层接口的动态代理机制,用于在运行时将接口方法映射到特定的 SQL 语句执行逻辑。

主要组件

  • SqlSession:SqlSession 是 MyBatis 的核心接口,用于执行 SQL 语句和管理事务。Mapper Proxy 通过 SqlSession 来执行数据库操作。
  • Mapper Proxy:MapperProxy 是 MyBatis 提供的动态代理实现类,用于代理 Mapper 接口。
  • Mapper Proxy Factory:MapperProxyFactory 是用于创建 MapperProxy 对象的工厂类。每个 Mapper 接口对应一个 MapperProxyFactory 实例。

优势与局限性

优势

  • 解耦:开发者只需定义接口和 SQL,无需关注具体实现。
  • 简洁性:代码量减少,尤其适合简单的 CRUD 操作。
  • 动态性:基于动态代理,无需生成实际的实现类。

局限性

  • 性能开销:动态代理在运行时生成对象,性能略低于手写实现类。
  • 可读性:当 Mapper 接口较多或方法复杂时,调试和维护可能变得困难。
  • 复杂逻辑限制:复杂查询和多表关联操作需要依赖 XML 或其他方式实现。

额外扩展

  • 使用 XML 配置 或 注解配置 定义 SQL。
  • 可以通过插件(Interceptors)扩展 MapperProxy 的行为,例如记录 SQL 执行时间、日志输出等。

运行机制

获取 Mapper 的实例

当通过 SqlSession.getMapper(Class type) 获取一个 Mapper 接口的实例时,MyBatis 会执行以下步骤:

  • 代理工厂创建代理对象:MyBatis 使用 MapperProxyFactory 来生成一个 MapperProxy 的动态代理对象。
  • MapperProxyFactory 内部调用 JDK 的动态代理工具生成代理类。
  • 生成的代理类实现了用户定义的 Mapper 接口。
  • 返回代理对象:该代理对象在表面上是用户定义的接口的实现类,但其方法调用会被拦截并处理。

方法调用拦截

当调用 Mapper 接口中的方法时,实际上会触发 MapperProxy 的 invoke 方法。

  • 判断方法所属:MapperProxy 会首先检查方法是否是 Object 类的方法(例如 toString、hashCode 等),这些方法会直接调用 Object 的实现。
  • 获取 SQL 映射:如果方法是 Mapper 接口的方法,MapperProxy 会通过 cachedMapperMethod 方法查找或创建 MapperMethod。MapperMethod 是一个内部工具类,表示 Mapper 接口方法和 SQL 语句的映射关系。
  • 执行 SQL 操作:通过 MapperMethod.execute() 方法调用对应的 SQL。

## SQL 执行

MapperMethod 执行时会调用 SqlSession 提供的数据库操作方法,例如:

  • selectOne:执行单条记录查询。
  • selectList:执行多条记录查询。
  • insert:执行插入操作。
  • update:执行更新操作。
  • delete:执行删除操作。

Mapper 代理

回顾一下我们之前的写法:

java 复制代码
public class WzkicuPage01 {

    public static void main(String[] args) throws Exception {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        PageHelper.startPage(2, 1);
        List<WzkUser> dataList = userMapper.selectList();
        for (WzkUser wzk : dataList) {
            System.out.println(wzk);
        }
        sqlSession.close();

    }

}

MyBatis 初始化时对接口的处理,MapperRegistry 是 Configuration 中的一个属性,它内部维护一个 HashMap 用于存放 mapper 接口的工厂类,每个接口对应一个工厂类,mappers 中可以配置接口的包路径,或者某个具体的接口类。

xml 复制代码
<mappers>
    <mapper resource="mapper.xml"/>
    <mapper resource="OrderMapper.xml"/>
    <mapper resource="UserMapper.xml"/>
    <mapper resource="RoleMapper.xml"/>
    <mapper resource="UserCacheMapper.xml"/>
</mappers>

当解析 mappers 标签的时候,它会判断解析到的 mapper 配置文件时,会将对应的配置文件中的增删改查封装成 MappedStatement 对象,存入到 MappedStatements 中。

java 复制代码
// DefaultSqlSession 中的 getMapper
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}

// Configuration 中的 getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

// MapperRegistry 中的 getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从 MapperRegistry 中的 HashMap 中获取 MapperProxyFactory
    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);
    }
}

// MapperProxyFactory 类中的 newInstance 方法
public T newInstance(SqlSession sqlSession) {
    // 创建 JDK 动态代理的 Handler 类
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    // 调用重载方法
    return newInstance(mapperProxy);
}

// MapperProxy 类,实现了 InvocationHandler 接口
public class MapperProxy<T> implements InvocationHandler, Serializable {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    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;
    }

    // 省略其他实现部分
}

invoke

在动态嗲里之后,执行时在 MapperProxy 中的 invoke 方法:

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 1. 如果是 Object 类中定义的方法,直接调用
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }

        // 2. 如果是默认方法,调用默认方法
        else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        // 3. 捕获并解包异常
        throw ExceptionUtil.unwrapThrowable(t);
    }

    // 4. 获取 MapperMethod 对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);

    // 5. 执行 MapperMethod 对应的方法
    return mapperMethod.execute(sqlSession, args);
}

其中的 execute 方法:

java 复制代码
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;

    // Determine the type of operation based on the command's type
    switch (command.getType()) {
        case INSERT:
            result = executeInsert(sqlSession, args);
            break;

        case UPDATE:
            result = executeUpdate(sqlSession, args);
            break;

        case DELETE:
            result = executeDelete(sqlSession, args);
            break;

        case SELECT:
            result = executeSelect(sqlSession, args);
            break;

        case FLUSH:
            result = sqlSession.flushStatements();
            break;

        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }

    // Check for null result and primitive return type, throw exception if necessary
    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;
}

private Object executeInsert(SqlSession sqlSession, Object[] args) {
    Object param = method.convertArgsToSqlCommandParam(args);
    int rowCount = sqlSession.insert(command.getName(), param);
    return rowCountResult(rowCount);
}

private Object executeUpdate(SqlSession sqlSession, Object[] args) {
    Object param = method.convertArgsToSqlCommandParam(args);
    int rowCount = sqlSession.update(command.getName(), param);
    return rowCountResult(rowCount);
}

private Object executeDelete(SqlSession sqlSession, Object[] args) {
    Object param = method.convertArgsToSqlCommandParam(args);
    int rowCount = sqlSession.delete(command.getName(), param);
    return rowCountResult(rowCount);
}

private Object executeSelect(SqlSession sqlSession, Object[] args) {
    if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        return null;
    } else if (method.returnsMany()) {
        return executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
        return executeForMap(sqlSession, args);
    } else if (method.returnsCursor()) {
        return executeForCursor(sqlSession, args);
    } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        Object result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
            return Optional.ofNullable(result);
        }
        return result;
    }
}

作者:武子康的个人博客

相关推荐
plainGeekDev1 小时前
CountDownTimer → Flow
android·java·kotlin
心之伊始1 小时前
Java 后端 AI 应用网关实战:多模型路由、Fallback、超时和可观测性设计
java·spring boot·大模型·架构设计·ai网关
牛栓柱1 小时前
【后端实战】用 Supabase + React/TS 零成本构建高并发 Multi-Agent 服务
前端·数据库·人工智能·后端·react.js·前端框架
wuhuhuan2 小时前
playwright java maven项目创建
后端
小锋java12342 小时前
【技术专题】LangChain4j 开发Java Agent智能体 - 嵌入模型与向量数据库
java·人工智能
卷无止境2 小时前
可靠性工程统计:让失效变得"可预测"
后端
卷无止境2 小时前
C# 中的 Event:让对象学会"开口说话"
后端
程序员皮皮林2 小时前
Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
java·开发语言·dubbo
小锋java12342 小时前
10分钟学会Java16新特性record
java